@forwardimpact/outpost 3.1.2 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fit-outpost.js +23 -2
- package/package.json +4 -3
- package/src/agent-runner.js +59 -19
- package/src/index.js +2 -2
- package/src/kb-manager.js +76 -75
- package/src/outpost.js +385 -340
- package/src/scheduler.js +47 -8
- package/src/socket-server.js +76 -45
- package/src/state-manager.js +32 -35
package/bin/fit-outpost.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Thin entry point —
|
|
2
|
+
// Thin entry point — the sole construction site for the runtime collaborator
|
|
3
|
+
// bag, threaded into src/outpost.js's dispatch via run(runtime, version).
|
|
3
4
|
import "@forwardimpact/libpreflight/node22";
|
|
4
5
|
|
|
5
|
-
import "
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
|
|
11
|
+
|
|
12
|
+
import { run } from "../src/outpost.js";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
// In compiled binaries (bun build --compile), `bun build --define` injects the
|
|
17
|
+
// version string here so the readFileSync branch is eliminated as dead code.
|
|
18
|
+
// Source execution falls through to package.json.
|
|
19
|
+
const VERSION =
|
|
20
|
+
process.env.OUTPOST_VERSION ||
|
|
21
|
+
JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"))
|
|
22
|
+
.version;
|
|
23
|
+
|
|
24
|
+
const runtime = createDefaultRuntime();
|
|
25
|
+
const code = await run(runtime, VERSION);
|
|
26
|
+
if (code) runtime.proc.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/outpost",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
4
4
|
"description": "Personal operations center — context from email, calendar, and knowledge assembled so preparation is continuous, not a morning scramble.",
|
|
5
5
|
"homepage": "https://www.forwardimpact.team",
|
|
6
6
|
"repository": {
|
|
@@ -52,10 +52,11 @@
|
|
|
52
52
|
"@forwardimpact/libcli": "^0.1.0",
|
|
53
53
|
"@forwardimpact/libmacos": "^0.1.0",
|
|
54
54
|
"@forwardimpact/libpreflight": "^0.1.0",
|
|
55
|
-
"@forwardimpact/libtelemetry": "^0.1.33"
|
|
55
|
+
"@forwardimpact/libtelemetry": "^0.1.33",
|
|
56
|
+
"@forwardimpact/libutil": "^0.1.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
|
-
"@forwardimpact/
|
|
59
|
+
"@forwardimpact/libmock": "^0.1.0"
|
|
59
60
|
},
|
|
60
61
|
"engines": {
|
|
61
62
|
"bun": ">=1.2.0",
|
package/src/agent-runner.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* AgentRunner — spawn agent process, capture output, update state.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
5
|
import { resolve, join } from "node:path";
|
|
7
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { isoTimestamp } from "@forwardimpact/libutil";
|
|
8
8
|
|
|
9
9
|
/** Spawn agent CLI processes, capture their output, and update agent state. */
|
|
10
10
|
export class AgentRunner {
|
|
@@ -13,22 +13,36 @@ export class AgentRunner {
|
|
|
13
13
|
#log;
|
|
14
14
|
#activeChildren;
|
|
15
15
|
#cacheDir;
|
|
16
|
+
#fs;
|
|
17
|
+
#proc;
|
|
18
|
+
#clock;
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
|
-
* @param {Object} spawn -
|
|
21
|
+
* @param {Object | (() => Object | Promise<{default?: Object}>)} spawn -
|
|
22
|
+
* The posix-spawn module, or a (possibly async) loader returning it. A
|
|
23
|
+
* loader lets the Bun-FFI module load lazily so plain `node` invocations
|
|
24
|
+
* that never wake an agent don't pull in `bun:ffi`.
|
|
19
25
|
* @param {import('./state-manager.js').StateManager} stateManager
|
|
20
26
|
* @param {Function} logFn - Logging function
|
|
21
27
|
* @param {string} cacheDir - Cache directory for state files
|
|
28
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
29
|
+
* Injected runtime bag (uses `fs` (async), `proc`, `clock`).
|
|
22
30
|
*/
|
|
23
|
-
constructor(spawn, stateManager, logFn, cacheDir) {
|
|
31
|
+
constructor(spawn, stateManager, logFn, cacheDir, runtime) {
|
|
24
32
|
if (!spawn) throw new Error("spawn is required");
|
|
25
33
|
if (!stateManager) throw new Error("stateManager is required");
|
|
26
34
|
if (!logFn) throw new Error("logFn is required");
|
|
27
35
|
if (!cacheDir) throw new Error("cacheDir is required");
|
|
36
|
+
if (!runtime?.fs) throw new Error("runtime.fs is required");
|
|
37
|
+
if (!runtime?.proc) throw new Error("runtime.proc is required");
|
|
38
|
+
if (!runtime?.clock) throw new Error("runtime.clock is required");
|
|
28
39
|
this.#spawn = spawn;
|
|
29
40
|
this.#stateManager = stateManager;
|
|
30
41
|
this.#log = logFn;
|
|
31
42
|
this.#cacheDir = cacheDir;
|
|
43
|
+
this.#fs = runtime.fs;
|
|
44
|
+
this.#proc = runtime.proc;
|
|
45
|
+
this.#clock = runtime.clock;
|
|
32
46
|
this.#activeChildren = new Set();
|
|
33
47
|
}
|
|
34
48
|
|
|
@@ -37,11 +51,36 @@ export class AgentRunner {
|
|
|
37
51
|
return this.#activeChildren;
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the injected spawn collaborator to the posix-spawn module,
|
|
56
|
+
* invoking and unwrapping a loader thunk on first use.
|
|
57
|
+
* @returns {Promise<Object>}
|
|
58
|
+
*/
|
|
59
|
+
async #resolveSpawn() {
|
|
60
|
+
if (typeof this.#spawn === "function" && !this.#spawn.spawn) {
|
|
61
|
+
const loaded = await this.#spawn();
|
|
62
|
+
this.#spawn = loaded?.spawn ? loaded : (loaded?.default ?? loaded);
|
|
63
|
+
}
|
|
64
|
+
return this.#spawn;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Test whether a path exists, via the async fs surface.
|
|
69
|
+
* @param {string} p
|
|
70
|
+
* @returns {Promise<boolean>}
|
|
71
|
+
*/
|
|
72
|
+
async #exists(p) {
|
|
73
|
+
return this.#fs.access(p).then(
|
|
74
|
+
() => true,
|
|
75
|
+
() => false,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
/**
|
|
41
80
|
* Find the claude CLI binary
|
|
42
|
-
* @returns {string}
|
|
81
|
+
* @returns {Promise<string>}
|
|
43
82
|
*/
|
|
44
|
-
#findClaude() {
|
|
83
|
+
async #findClaude() {
|
|
45
84
|
const HOME = homedir();
|
|
46
85
|
const paths = [
|
|
47
86
|
"/usr/local/bin/claude",
|
|
@@ -49,7 +88,7 @@ export class AgentRunner {
|
|
|
49
88
|
join(HOME, ".local", "bin", "claude"),
|
|
50
89
|
"/opt/homebrew/bin/claude",
|
|
51
90
|
];
|
|
52
|
-
for (const p of paths) if (
|
|
91
|
+
for (const p of paths) if (await this.#exists(p)) return p;
|
|
53
92
|
return "claude";
|
|
54
93
|
}
|
|
55
94
|
|
|
@@ -71,7 +110,7 @@ export class AgentRunner {
|
|
|
71
110
|
Object.assign(agentState, {
|
|
72
111
|
status: "failed",
|
|
73
112
|
startedAt: null,
|
|
74
|
-
lastWokeAt:
|
|
113
|
+
lastWokeAt: isoTimestamp(this.#clock.now()),
|
|
75
114
|
lastError: String(error).slice(0, 500),
|
|
76
115
|
});
|
|
77
116
|
}
|
|
@@ -84,7 +123,7 @@ export class AgentRunner {
|
|
|
84
123
|
* @returns {Record<string, string>}
|
|
85
124
|
*/
|
|
86
125
|
#buildSpawnEnv(configEnv) {
|
|
87
|
-
const env = { ...
|
|
126
|
+
const env = { ...this.#proc.env };
|
|
88
127
|
if (configEnv) {
|
|
89
128
|
const home = homedir();
|
|
90
129
|
for (const [key, value] of Object.entries(configEnv)) {
|
|
@@ -108,21 +147,21 @@ export class AgentRunner {
|
|
|
108
147
|
return;
|
|
109
148
|
}
|
|
110
149
|
const kbPath = this.#expandPath(agent.kb);
|
|
111
|
-
if (!
|
|
150
|
+
if (!(await this.#exists(kbPath))) {
|
|
112
151
|
this.#log(
|
|
113
152
|
`Agent ${agentName}: path "${kbPath}" does not exist, skipping.`,
|
|
114
153
|
);
|
|
115
154
|
return;
|
|
116
155
|
}
|
|
117
156
|
|
|
118
|
-
const claude = this.#findClaude();
|
|
157
|
+
const claude = await this.#findClaude();
|
|
119
158
|
|
|
120
159
|
this.#log(`Waking agent: ${agentName} (kb: ${agent.kb})`);
|
|
121
160
|
|
|
122
161
|
const as = (state.agents[agentName] ||= {});
|
|
123
162
|
as.status = "active";
|
|
124
|
-
as.startedAt =
|
|
125
|
-
this.#stateManager.save(state);
|
|
163
|
+
as.startedAt = isoTimestamp(this.#clock.now());
|
|
164
|
+
await this.#stateManager.save(state);
|
|
126
165
|
|
|
127
166
|
const spawnArgs = [
|
|
128
167
|
"--chrome",
|
|
@@ -134,9 +173,10 @@ export class AgentRunner {
|
|
|
134
173
|
];
|
|
135
174
|
|
|
136
175
|
const env = this.#buildSpawnEnv(configEnv);
|
|
176
|
+
const spawnMod = await this.#resolveSpawn();
|
|
137
177
|
|
|
138
178
|
try {
|
|
139
|
-
const { pid, stdoutFile, stderrFile } =
|
|
179
|
+
const { pid, stdoutFile, stderrFile } = spawnMod.spawn(
|
|
140
180
|
claude,
|
|
141
181
|
spawnArgs,
|
|
142
182
|
env,
|
|
@@ -144,17 +184,17 @@ export class AgentRunner {
|
|
|
144
184
|
);
|
|
145
185
|
this.#activeChildren.add(pid);
|
|
146
186
|
|
|
147
|
-
const exitCode = await
|
|
187
|
+
const exitCode = await spawnMod.waitForExit(pid);
|
|
148
188
|
this.#activeChildren.delete(pid);
|
|
149
189
|
|
|
150
|
-
const stdout =
|
|
151
|
-
const stderr =
|
|
190
|
+
const stdout = spawnMod.readOutput(stdoutFile);
|
|
191
|
+
const stderr = spawnMod.readOutput(stderrFile);
|
|
152
192
|
|
|
153
193
|
if (exitCode === 0) {
|
|
154
194
|
this.#log(
|
|
155
195
|
`Agent ${agentName} completed. Output: ${stdout.slice(0, 200)}...`,
|
|
156
196
|
);
|
|
157
|
-
this.#stateManager.updateAgentState(
|
|
197
|
+
await this.#stateManager.updateAgentState(
|
|
158
198
|
as,
|
|
159
199
|
stdout,
|
|
160
200
|
agentName,
|
|
@@ -169,7 +209,7 @@ export class AgentRunner {
|
|
|
169
209
|
this.#log(`Agent ${agentName} failed: ${err.message}`);
|
|
170
210
|
this.#failAgent(as, err.message);
|
|
171
211
|
}
|
|
172
|
-
this.#stateManager.save(state);
|
|
212
|
+
await this.#stateManager.save(state);
|
|
173
213
|
}
|
|
174
214
|
|
|
175
215
|
/**
|
|
@@ -178,7 +218,7 @@ export class AgentRunner {
|
|
|
178
218
|
killActiveChildren() {
|
|
179
219
|
for (const pid of this.#activeChildren) {
|
|
180
220
|
try {
|
|
181
|
-
|
|
221
|
+
this.#proc.kill(pid, "SIGTERM");
|
|
182
222
|
this.#log(`Sent SIGTERM to child PID ${pid}`);
|
|
183
223
|
} catch {
|
|
184
224
|
// Already exited
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Public entry point for @forwardimpact/outpost.
|
|
2
2
|
// Outpost is primarily a CLI — this file exists so the package conforms
|
|
3
|
-
// to the repo-wide layout contract
|
|
4
|
-
//
|
|
3
|
+
// to the repo-wide layout contract. The runtime CLI dispatch lives in
|
|
4
|
+
// src/outpost.js.
|
|
5
5
|
export * from "./outpost.js";
|
package/src/kb-manager.js
CHANGED
|
@@ -2,15 +2,6 @@
|
|
|
2
2
|
* KBManager — knowledge base init/update operations.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
existsSync,
|
|
7
|
-
mkdirSync,
|
|
8
|
-
copyFileSync,
|
|
9
|
-
cpSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
writeFileSync,
|
|
12
|
-
readdirSync,
|
|
13
|
-
} from "node:fs";
|
|
14
5
|
import { join, dirname, resolve } from "node:path";
|
|
15
6
|
import { homedir } from "node:os";
|
|
16
7
|
import { createLogger } from "@forwardimpact/libtelemetry";
|
|
@@ -22,30 +13,44 @@ export class KBManager {
|
|
|
22
13
|
#fs;
|
|
23
14
|
|
|
24
15
|
/**
|
|
25
|
-
* @param {
|
|
16
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
17
|
+
* Injected runtime bag (uses `fs` (async)).
|
|
26
18
|
* @param {Function} logFn
|
|
27
19
|
*/
|
|
28
|
-
constructor(
|
|
29
|
-
if (!fs) throw new Error("fs is required");
|
|
20
|
+
constructor(runtime, logFn) {
|
|
21
|
+
if (!runtime?.fs) throw new Error("runtime.fs is required");
|
|
30
22
|
if (!logFn) throw new Error("logFn is required");
|
|
31
|
-
this.#fs = fs;
|
|
23
|
+
this.#fs = runtime.fs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Test whether a path exists, via the async fs surface.
|
|
28
|
+
* @param {string} p
|
|
29
|
+
* @returns {Promise<boolean>}
|
|
30
|
+
*/
|
|
31
|
+
async #exists(p) {
|
|
32
|
+
return this.#fs.access(p).then(
|
|
33
|
+
() => true,
|
|
34
|
+
() => false,
|
|
35
|
+
);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* @param {string} dir
|
|
40
|
+
* @returns {Promise<void>}
|
|
36
41
|
*/
|
|
37
|
-
#ensureDir(dir) {
|
|
38
|
-
this.#fs.
|
|
42
|
+
async #ensureDir(dir) {
|
|
43
|
+
await this.#fs.mkdir(dir, { recursive: true });
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
47
|
* @param {string} path
|
|
43
48
|
* @param {*} fallback
|
|
44
|
-
* @returns {
|
|
49
|
+
* @returns {Promise<*>}
|
|
45
50
|
*/
|
|
46
|
-
#readJSON(path, fallback) {
|
|
51
|
+
async #readJSON(path, fallback) {
|
|
47
52
|
try {
|
|
48
|
-
return JSON.parse(this.#fs.
|
|
53
|
+
return JSON.parse(await this.#fs.readFile(path, "utf8"));
|
|
49
54
|
} catch {
|
|
50
55
|
return fallback;
|
|
51
56
|
}
|
|
@@ -54,38 +59,40 @@ export class KBManager {
|
|
|
54
59
|
/**
|
|
55
60
|
* @param {string} path
|
|
56
61
|
* @param {*} data
|
|
62
|
+
* @returns {Promise<void>}
|
|
57
63
|
*/
|
|
58
|
-
#writeJSON(path, data) {
|
|
59
|
-
this.#ensureDir(dirname(path));
|
|
60
|
-
this.#fs.
|
|
64
|
+
async #writeJSON(path, data) {
|
|
65
|
+
await this.#ensureDir(dirname(path));
|
|
66
|
+
await this.#fs.writeFile(path, JSON.stringify(data, null, 2) + "\n");
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* Copy bundled files (CLAUDE.md, skills, agents) from template to a KB.
|
|
65
71
|
* @param {string} tpl - Path to the template directory
|
|
66
72
|
* @param {string} dest - Path to the target knowledge base
|
|
73
|
+
* @returns {Promise<void>}
|
|
67
74
|
*/
|
|
68
|
-
copyBundledFiles(tpl, dest) {
|
|
69
|
-
this.#fs.
|
|
75
|
+
async copyBundledFiles(tpl, dest) {
|
|
76
|
+
await this.#fs.copyFile(join(tpl, "CLAUDE.md"), join(dest, "CLAUDE.md"));
|
|
70
77
|
logger.info(` Updated CLAUDE.md`);
|
|
71
78
|
|
|
72
79
|
const apmSrc = join(tpl, "apm.yml");
|
|
73
|
-
if (this.#
|
|
74
|
-
this.#fs.
|
|
80
|
+
if (await this.#exists(apmSrc)) {
|
|
81
|
+
await this.#fs.copyFile(apmSrc, join(dest, "apm.yml"));
|
|
75
82
|
logger.info(` Updated apm.yml`);
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
this.mergeSettings(tpl, dest);
|
|
85
|
+
await this.mergeSettings(tpl, dest);
|
|
79
86
|
|
|
80
87
|
for (const sub of ["skills", "agents"]) {
|
|
81
88
|
const src = join(tpl, ".claude", sub);
|
|
82
|
-
if (!this.#
|
|
83
|
-
this.#fs.
|
|
84
|
-
const entries =
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
if (!(await this.#exists(src))) continue;
|
|
90
|
+
await this.#fs.cp(src, join(dest, ".claude", sub), { recursive: true });
|
|
91
|
+
const entries = (
|
|
92
|
+
await this.#fs.readdir(src, { withFileTypes: true })
|
|
93
|
+
).filter((d) =>
|
|
94
|
+
sub === "skills" ? d.isDirectory() : d.name.endsWith(".md"),
|
|
95
|
+
);
|
|
89
96
|
const names = entries.map((d) =>
|
|
90
97
|
sub === "agents" ? d.name.replace(".md", "") : d.name,
|
|
91
98
|
);
|
|
@@ -123,29 +130,30 @@ export class KBManager {
|
|
|
123
130
|
* Merge template settings.json into the destination's settings.json.
|
|
124
131
|
* @param {string} tpl - Template directory
|
|
125
132
|
* @param {string} dest - Knowledge base directory
|
|
133
|
+
* @returns {Promise<void>}
|
|
126
134
|
*/
|
|
127
|
-
mergeSettings(tpl, dest) {
|
|
135
|
+
async mergeSettings(tpl, dest) {
|
|
128
136
|
const src = join(tpl, ".claude", "settings.json");
|
|
129
|
-
if (!this.#
|
|
137
|
+
if (!(await this.#exists(src))) return;
|
|
130
138
|
|
|
131
139
|
const destPath = join(dest, ".claude", "settings.json");
|
|
132
140
|
|
|
133
|
-
if (!this.#
|
|
134
|
-
this.#ensureDir(join(dest, ".claude"));
|
|
135
|
-
this.#fs.
|
|
141
|
+
if (!(await this.#exists(destPath))) {
|
|
142
|
+
await this.#ensureDir(join(dest, ".claude"));
|
|
143
|
+
await this.#fs.copyFile(src, destPath);
|
|
136
144
|
logger.info(` Created settings.json`);
|
|
137
145
|
return;
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
const template = this.#readJSON(src, {});
|
|
141
|
-
const existing = this.#readJSON(destPath, {});
|
|
148
|
+
const template = await this.#readJSON(src, {});
|
|
149
|
+
const existing = await this.#readJSON(destPath, {});
|
|
142
150
|
const added = this.#mergePermissionLists(
|
|
143
151
|
template.permissions || {},
|
|
144
152
|
(existing.permissions ||= {}),
|
|
145
153
|
);
|
|
146
154
|
|
|
147
155
|
if (added > 0) {
|
|
148
|
-
this.#writeJSON(destPath, existing);
|
|
156
|
+
await this.#writeJSON(destPath, existing);
|
|
149
157
|
logger.info(` Updated settings.json (${added} new entries)`);
|
|
150
158
|
} else {
|
|
151
159
|
logger.info(` Settings up to date`);
|
|
@@ -153,18 +161,22 @@ export class KBManager {
|
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
/**
|
|
156
|
-
* Initialize a new knowledge base
|
|
164
|
+
* Initialize a new knowledge base.
|
|
157
165
|
* @param {string} targetPath
|
|
158
166
|
* @param {string} templateDir
|
|
167
|
+
* @returns {Promise<{ok: true, value: {dest: string}} | {ok: false, code: number, error: string}>}
|
|
159
168
|
*/
|
|
160
|
-
init(targetPath, templateDir) {
|
|
169
|
+
async init(targetPath, templateDir) {
|
|
161
170
|
const dest = this.#expandPath(targetPath);
|
|
162
|
-
if (this.#
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
if (await this.#exists(join(dest, "CLAUDE.md"))) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
code: 1,
|
|
175
|
+
error: `Knowledge base already exists at ${dest}`,
|
|
176
|
+
};
|
|
165
177
|
}
|
|
166
178
|
|
|
167
|
-
this.#ensureDir(dest);
|
|
179
|
+
await this.#ensureDir(dest);
|
|
168
180
|
for (const d of [
|
|
169
181
|
"knowledge/People",
|
|
170
182
|
"knowledge/Organizations",
|
|
@@ -172,30 +184,39 @@ export class KBManager {
|
|
|
172
184
|
"knowledge/Topics",
|
|
173
185
|
"knowledge/Briefings",
|
|
174
186
|
])
|
|
175
|
-
this.#ensureDir(join(dest, d));
|
|
187
|
+
await this.#ensureDir(join(dest, d));
|
|
176
188
|
|
|
177
|
-
this.#fs.
|
|
189
|
+
await this.#fs.copyFile(
|
|
190
|
+
join(templateDir, "USER.md"),
|
|
191
|
+
join(dest, "USER.md"),
|
|
192
|
+
);
|
|
178
193
|
|
|
179
|
-
this.copyBundledFiles(templateDir, dest);
|
|
194
|
+
await this.copyBundledFiles(templateDir, dest);
|
|
180
195
|
|
|
181
196
|
logger.info(
|
|
182
197
|
`Knowledge base initialized at ${dest}\n\nNext steps:\n 1. Edit ${dest}/USER.md with your name, email, and domain\n 2. cd ${dest} && npx apm install\n 3. claude`,
|
|
183
198
|
);
|
|
199
|
+
return { ok: true, value: { dest } };
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
/**
|
|
187
203
|
* Update an existing knowledge base with the latest bundled files.
|
|
188
204
|
* @param {string} targetPath
|
|
189
205
|
* @param {string} templateDir
|
|
206
|
+
* @returns {Promise<{ok: true, value: {dest: string}} | {ok: false, code: number, error: string}>}
|
|
190
207
|
*/
|
|
191
|
-
update(targetPath, templateDir) {
|
|
208
|
+
async update(targetPath, templateDir) {
|
|
192
209
|
const dest = this.#expandPath(targetPath);
|
|
193
|
-
if (!this.#
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
if (!(await this.#exists(join(dest, "CLAUDE.md")))) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
code: 1,
|
|
214
|
+
error: `No knowledge base found at ${dest}`,
|
|
215
|
+
};
|
|
196
216
|
}
|
|
197
|
-
this.copyBundledFiles(templateDir, dest);
|
|
217
|
+
await this.copyBundledFiles(templateDir, dest);
|
|
198
218
|
logger.info(`\nKnowledge base updated: ${dest}`);
|
|
219
|
+
return { ok: true, value: { dest } };
|
|
199
220
|
}
|
|
200
221
|
|
|
201
222
|
/**
|
|
@@ -206,23 +227,3 @@ export class KBManager {
|
|
|
206
227
|
return p.startsWith("~/") ? join(homedir(), p.slice(2)) : resolve(p);
|
|
207
228
|
}
|
|
208
229
|
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Create a KBManager with real fs dependencies
|
|
212
|
-
* @param {Function} logFn
|
|
213
|
-
* @returns {KBManager}
|
|
214
|
-
*/
|
|
215
|
-
export function createKBManager(logFn) {
|
|
216
|
-
return new KBManager(
|
|
217
|
-
{
|
|
218
|
-
existsSync,
|
|
219
|
-
mkdirSync,
|
|
220
|
-
copyFileSync,
|
|
221
|
-
cpSync,
|
|
222
|
-
readFileSync,
|
|
223
|
-
writeFileSync,
|
|
224
|
-
readdirSync,
|
|
225
|
-
},
|
|
226
|
-
logFn,
|
|
227
|
-
);
|
|
228
|
-
}
|