akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
86 lines (85 loc) • 3.31 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* Shared skeleton for {@link SessionLogHarness} providers (§4.6 dedup).
*
* Holds only what the concrete providers genuinely share: safe stat'ing,
* recursive directory walking, the mtime-filtered file→summary listing loop,
* and conditional-spread assembly of
* {@link SessionSummary} refs stamped with the provider's runtime name.
* Everything platform-specific — file layouts, metadata peeking, SQLite
* stores, message flattening — stays in the subclasses.
*/
import fs from "node:fs";
import path from "node:path";
export class AbstractSessionLogProvider {
isAvailable() {
return fs.existsSync(this.availabilityRoot());
}
/** `fs.statSync` that returns `undefined` instead of throwing. */
statSafe(target) {
try {
return fs.statSync(target);
}
catch {
return undefined;
}
}
/** Recursively yield files under `dir` whose basename passes `matches`. */
*walkFiles(dir, matches) {
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory())
yield* this.walkFiles(full, matches);
else if (matches(entry.name))
yield full;
}
}
catch {
// permission errors etc.
}
}
/**
* Assemble a {@link SessionSummary} stamped with this provider's name.
* Timestamps are included whenever defined (0 is valid); `projectHint` /
* `title` only when non-empty, so absent metadata stays absent.
*/
sessionRef(input) {
return {
harness: this.name,
sessionId: input.sessionId,
filePath: input.filePath,
...(input.startedAt !== undefined ? { startedAt: input.startedAt } : {}),
...(input.endedAt !== undefined ? { endedAt: input.endedAt } : {}),
...(input.projectHint ? { projectHint: input.projectHint } : {}),
...(input.title ? { title: input.title } : {}),
};
}
/**
* The shared listing loop: enumerate candidate session files, drop the
* unstat'able and the ones older than `sinceMs`, summarize the rest, and
* sort newest-ended first. An enumeration failure (root missing or
* unreadable) returns what was collected so far rather than throwing.
*/
listSessionsFromFiles(input) {
const summaries = [];
try {
for (const filePath of input.enumerate()) {
const stat = this.statSafe(filePath);
if (!stat)
continue;
if (stat.mtimeMs < input.sinceMs)
continue;
const summary = input.summarize(filePath, stat);
if (summary)
summaries.push(summary);
}
}
catch {
// Root missing or unreadable — return what we have.
}
return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
}
}