@forwardimpact/outpost 3.1.3 → 3.2.0
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 +14 -2
- package/package.json +9 -3
- package/src/agent-runner.js +66 -19
- package/src/kb-manager.js +88 -85
- package/src/outpost.js +386 -342
- package/src/scheduler.js +47 -8
- package/src/socket-server.js +77 -47
- package/src/state-manager.js +32 -35
package/bin/fit-outpost.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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 { resolveVersion } from "@forwardimpact/libcli";
|
|
7
|
+
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
|
|
8
|
+
|
|
9
|
+
import { run } from "../src/outpost.js";
|
|
10
|
+
|
|
11
|
+
const runtime = createDefaultRuntime();
|
|
12
|
+
const version = resolveVersion({
|
|
13
|
+
packageJsonUrl: new URL("../package.json", import.meta.url),
|
|
14
|
+
runtime,
|
|
15
|
+
});
|
|
16
|
+
const code = await run(runtime, version);
|
|
17
|
+
if (code) runtime.proc.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/outpost",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
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": {
|
|
@@ -50,17 +50,23 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@forwardimpact/libcli": "^0.1.0",
|
|
53
|
-
"@forwardimpact/libmacos": "^0.1.0",
|
|
54
53
|
"@forwardimpact/libpreflight": "^0.1.0",
|
|
55
|
-
"@forwardimpact/libtelemetry": "^0.1.33"
|
|
54
|
+
"@forwardimpact/libtelemetry": "^0.1.33",
|
|
55
|
+
"@forwardimpact/libutil": "^0.1.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@forwardimpact/libmock": "^0.1.0"
|
|
59
59
|
},
|
|
60
|
+
"optionalDependencies": {
|
|
61
|
+
"@forwardimpact/libmacos": "^0.1.0"
|
|
62
|
+
},
|
|
60
63
|
"engines": {
|
|
61
64
|
"bun": ">=1.2.0",
|
|
62
65
|
"node": ">=22.0.0"
|
|
63
66
|
},
|
|
67
|
+
"os": [
|
|
68
|
+
"darwin"
|
|
69
|
+
],
|
|
64
70
|
"publishConfig": {
|
|
65
71
|
"access": "public"
|
|
66
72
|
}
|
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,38 @@ export class AgentRunner {
|
|
|
13
13
|
#log;
|
|
14
14
|
#activeChildren;
|
|
15
15
|
#cacheDir;
|
|
16
|
+
#fs;
|
|
17
|
+
#proc;
|
|
18
|
+
#clock;
|
|
19
|
+
#runtime;
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
* @param {Object} spawn -
|
|
22
|
+
* @param {Object | (() => Object | Promise<{default?: Object}>)} spawn -
|
|
23
|
+
* The posix-spawn module, or a (possibly async) loader returning it. A
|
|
24
|
+
* loader lets the Bun-FFI module load lazily so plain `node` invocations
|
|
25
|
+
* that never wake an agent don't pull in `bun:ffi`.
|
|
19
26
|
* @param {import('./state-manager.js').StateManager} stateManager
|
|
20
27
|
* @param {Function} logFn - Logging function
|
|
21
28
|
* @param {string} cacheDir - Cache directory for state files
|
|
29
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
30
|
+
* Injected runtime bag (uses `fs` (async), `proc`, `clock`).
|
|
22
31
|
*/
|
|
23
|
-
constructor(spawn, stateManager, logFn, cacheDir) {
|
|
32
|
+
constructor(spawn, stateManager, logFn, cacheDir, runtime) {
|
|
24
33
|
if (!spawn) throw new Error("spawn is required");
|
|
25
34
|
if (!stateManager) throw new Error("stateManager is required");
|
|
26
35
|
if (!logFn) throw new Error("logFn is required");
|
|
27
36
|
if (!cacheDir) throw new Error("cacheDir is required");
|
|
37
|
+
if (!runtime?.fs) throw new Error("runtime.fs is required");
|
|
38
|
+
if (!runtime?.proc) throw new Error("runtime.proc is required");
|
|
39
|
+
if (!runtime?.clock) throw new Error("runtime.clock is required");
|
|
28
40
|
this.#spawn = spawn;
|
|
29
41
|
this.#stateManager = stateManager;
|
|
30
42
|
this.#log = logFn;
|
|
31
43
|
this.#cacheDir = cacheDir;
|
|
44
|
+
this.#fs = runtime.fs;
|
|
45
|
+
this.#proc = runtime.proc;
|
|
46
|
+
this.#clock = runtime.clock;
|
|
47
|
+
this.#runtime = runtime;
|
|
32
48
|
this.#activeChildren = new Set();
|
|
33
49
|
}
|
|
34
50
|
|
|
@@ -37,11 +53,36 @@ export class AgentRunner {
|
|
|
37
53
|
return this.#activeChildren;
|
|
38
54
|
}
|
|
39
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the injected spawn collaborator to the posix-spawn module,
|
|
58
|
+
* invoking and unwrapping a loader thunk on first use.
|
|
59
|
+
* @returns {Promise<Object>}
|
|
60
|
+
*/
|
|
61
|
+
async #resolveSpawn() {
|
|
62
|
+
if (typeof this.#spawn === "function" && !this.#spawn.spawn) {
|
|
63
|
+
const loaded = await this.#spawn();
|
|
64
|
+
this.#spawn = loaded?.spawn ? loaded : (loaded?.default ?? loaded);
|
|
65
|
+
}
|
|
66
|
+
return this.#spawn;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Test whether a path exists, via the async fs surface.
|
|
71
|
+
* @param {string} p
|
|
72
|
+
* @returns {Promise<boolean>}
|
|
73
|
+
*/
|
|
74
|
+
async #exists(p) {
|
|
75
|
+
return this.#fs.access(p).then(
|
|
76
|
+
() => true,
|
|
77
|
+
() => false,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
/**
|
|
41
82
|
* Find the claude CLI binary
|
|
42
|
-
* @returns {string}
|
|
83
|
+
* @returns {Promise<string>}
|
|
43
84
|
*/
|
|
44
|
-
#findClaude() {
|
|
85
|
+
async #findClaude() {
|
|
45
86
|
const HOME = homedir();
|
|
46
87
|
const paths = [
|
|
47
88
|
"/usr/local/bin/claude",
|
|
@@ -49,7 +90,7 @@ export class AgentRunner {
|
|
|
49
90
|
join(HOME, ".local", "bin", "claude"),
|
|
50
91
|
"/opt/homebrew/bin/claude",
|
|
51
92
|
];
|
|
52
|
-
for (const p of paths) if (
|
|
93
|
+
for (const p of paths) if (await this.#exists(p)) return p;
|
|
53
94
|
return "claude";
|
|
54
95
|
}
|
|
55
96
|
|
|
@@ -71,7 +112,7 @@ export class AgentRunner {
|
|
|
71
112
|
Object.assign(agentState, {
|
|
72
113
|
status: "failed",
|
|
73
114
|
startedAt: null,
|
|
74
|
-
lastWokeAt:
|
|
115
|
+
lastWokeAt: isoTimestamp(this.#clock.now()),
|
|
75
116
|
lastError: String(error).slice(0, 500),
|
|
76
117
|
});
|
|
77
118
|
}
|
|
@@ -84,7 +125,7 @@ export class AgentRunner {
|
|
|
84
125
|
* @returns {Record<string, string>}
|
|
85
126
|
*/
|
|
86
127
|
#buildSpawnEnv(configEnv) {
|
|
87
|
-
const env = { ...
|
|
128
|
+
const env = { ...this.#proc.env };
|
|
88
129
|
if (configEnv) {
|
|
89
130
|
const home = homedir();
|
|
90
131
|
for (const [key, value] of Object.entries(configEnv)) {
|
|
@@ -108,21 +149,21 @@ export class AgentRunner {
|
|
|
108
149
|
return;
|
|
109
150
|
}
|
|
110
151
|
const kbPath = this.#expandPath(agent.kb);
|
|
111
|
-
if (!
|
|
152
|
+
if (!(await this.#exists(kbPath))) {
|
|
112
153
|
this.#log(
|
|
113
154
|
`Agent ${agentName}: path "${kbPath}" does not exist, skipping.`,
|
|
114
155
|
);
|
|
115
156
|
return;
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
const claude = this.#findClaude();
|
|
159
|
+
const claude = await this.#findClaude();
|
|
119
160
|
|
|
120
161
|
this.#log(`Waking agent: ${agentName} (kb: ${agent.kb})`);
|
|
121
162
|
|
|
122
163
|
const as = (state.agents[agentName] ||= {});
|
|
123
164
|
as.status = "active";
|
|
124
|
-
as.startedAt =
|
|
125
|
-
this.#stateManager.save(state);
|
|
165
|
+
as.startedAt = isoTimestamp(this.#clock.now());
|
|
166
|
+
await this.#stateManager.save(state);
|
|
126
167
|
|
|
127
168
|
const spawnArgs = [
|
|
128
169
|
"--chrome",
|
|
@@ -134,27 +175,33 @@ export class AgentRunner {
|
|
|
134
175
|
];
|
|
135
176
|
|
|
136
177
|
const env = this.#buildSpawnEnv(configEnv);
|
|
178
|
+
const spawnMod = await this.#resolveSpawn();
|
|
137
179
|
|
|
138
180
|
try {
|
|
139
|
-
const { pid, stdoutFile, stderrFile } =
|
|
181
|
+
const { pid, stdoutFile, stderrFile } = spawnMod.spawn(
|
|
140
182
|
claude,
|
|
141
183
|
spawnArgs,
|
|
142
184
|
env,
|
|
143
185
|
kbPath,
|
|
186
|
+
this.#runtime,
|
|
144
187
|
);
|
|
145
188
|
this.#activeChildren.add(pid);
|
|
146
189
|
|
|
147
|
-
const exitCode = await
|
|
190
|
+
const exitCode = await spawnMod.waitForExit(
|
|
191
|
+
pid,
|
|
192
|
+
undefined,
|
|
193
|
+
this.#runtime,
|
|
194
|
+
);
|
|
148
195
|
this.#activeChildren.delete(pid);
|
|
149
196
|
|
|
150
|
-
const stdout =
|
|
151
|
-
const stderr =
|
|
197
|
+
const stdout = spawnMod.readOutput(stdoutFile, this.#runtime);
|
|
198
|
+
const stderr = spawnMod.readOutput(stderrFile, this.#runtime);
|
|
152
199
|
|
|
153
200
|
if (exitCode === 0) {
|
|
154
201
|
this.#log(
|
|
155
202
|
`Agent ${agentName} completed. Output: ${stdout.slice(0, 200)}...`,
|
|
156
203
|
);
|
|
157
|
-
this.#stateManager.updateAgentState(
|
|
204
|
+
await this.#stateManager.updateAgentState(
|
|
158
205
|
as,
|
|
159
206
|
stdout,
|
|
160
207
|
agentName,
|
|
@@ -169,7 +216,7 @@ export class AgentRunner {
|
|
|
169
216
|
this.#log(`Agent ${agentName} failed: ${err.message}`);
|
|
170
217
|
this.#failAgent(as, err.message);
|
|
171
218
|
}
|
|
172
|
-
this.#stateManager.save(state);
|
|
219
|
+
await this.#stateManager.save(state);
|
|
173
220
|
}
|
|
174
221
|
|
|
175
222
|
/**
|
|
@@ -178,7 +225,7 @@ export class AgentRunner {
|
|
|
178
225
|
killActiveChildren() {
|
|
179
226
|
for (const pid of this.#activeChildren) {
|
|
180
227
|
try {
|
|
181
|
-
|
|
228
|
+
this.#proc.kill(pid, "SIGTERM");
|
|
182
229
|
this.#log(`Sent SIGTERM to child PID ${pid}`);
|
|
183
230
|
} catch {
|
|
184
231
|
// Already exited
|
package/src/kb-manager.js
CHANGED
|
@@ -2,50 +2,55 @@
|
|
|
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";
|
|
17
8
|
|
|
18
|
-
const logger = createLogger("outpost");
|
|
19
|
-
|
|
20
9
|
/** Manage knowledge base lifecycle including initialization, updates, and settings merging. */
|
|
21
10
|
export class KBManager {
|
|
22
11
|
#fs;
|
|
12
|
+
#logger;
|
|
23
13
|
|
|
24
14
|
/**
|
|
25
|
-
* @param {
|
|
15
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
16
|
+
* Injected runtime bag (uses `fs` (async)).
|
|
26
17
|
* @param {Function} logFn
|
|
27
18
|
*/
|
|
28
|
-
constructor(
|
|
29
|
-
if (!fs) throw new Error("fs is required");
|
|
19
|
+
constructor(runtime, logFn) {
|
|
20
|
+
if (!runtime?.fs) throw new Error("runtime.fs is required");
|
|
30
21
|
if (!logFn) throw new Error("logFn is required");
|
|
31
|
-
this.#fs = fs;
|
|
22
|
+
this.#fs = runtime.fs;
|
|
23
|
+
this.#logger = createLogger("outpost", runtime);
|
|
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,42 +59,46 @@ 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.
|
|
70
|
-
logger.info(` Updated CLAUDE.md`);
|
|
75
|
+
async copyBundledFiles(tpl, dest) {
|
|
76
|
+
await this.#fs.copyFile(join(tpl, "CLAUDE.md"), join(dest, "CLAUDE.md"));
|
|
77
|
+
this.#logger.info(` Updated CLAUDE.md`);
|
|
71
78
|
|
|
72
79
|
const apmSrc = join(tpl, "apm.yml");
|
|
73
|
-
if (this.#
|
|
74
|
-
this.#fs.
|
|
75
|
-
logger.info(` Updated apm.yml`);
|
|
80
|
+
if (await this.#exists(apmSrc)) {
|
|
81
|
+
await this.#fs.copyFile(apmSrc, join(dest, "apm.yml"));
|
|
82
|
+
this.#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
|
);
|
|
92
|
-
logger.info(
|
|
99
|
+
this.#logger.info(
|
|
100
|
+
` Updated ${names.length} ${sub}: ${names.join(", ")}`,
|
|
101
|
+
);
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -123,48 +132,53 @@ export class KBManager {
|
|
|
123
132
|
* Merge template settings.json into the destination's settings.json.
|
|
124
133
|
* @param {string} tpl - Template directory
|
|
125
134
|
* @param {string} dest - Knowledge base directory
|
|
135
|
+
* @returns {Promise<void>}
|
|
126
136
|
*/
|
|
127
|
-
mergeSettings(tpl, dest) {
|
|
137
|
+
async mergeSettings(tpl, dest) {
|
|
128
138
|
const src = join(tpl, ".claude", "settings.json");
|
|
129
|
-
if (!this.#
|
|
139
|
+
if (!(await this.#exists(src))) return;
|
|
130
140
|
|
|
131
141
|
const destPath = join(dest, ".claude", "settings.json");
|
|
132
142
|
|
|
133
|
-
if (!this.#
|
|
134
|
-
this.#ensureDir(join(dest, ".claude"));
|
|
135
|
-
this.#fs.
|
|
136
|
-
logger.info(` Created settings.json`);
|
|
143
|
+
if (!(await this.#exists(destPath))) {
|
|
144
|
+
await this.#ensureDir(join(dest, ".claude"));
|
|
145
|
+
await this.#fs.copyFile(src, destPath);
|
|
146
|
+
this.#logger.info(` Created settings.json`);
|
|
137
147
|
return;
|
|
138
148
|
}
|
|
139
149
|
|
|
140
|
-
const template = this.#readJSON(src, {});
|
|
141
|
-
const existing = this.#readJSON(destPath, {});
|
|
150
|
+
const template = await this.#readJSON(src, {});
|
|
151
|
+
const existing = await this.#readJSON(destPath, {});
|
|
142
152
|
const added = this.#mergePermissionLists(
|
|
143
153
|
template.permissions || {},
|
|
144
154
|
(existing.permissions ||= {}),
|
|
145
155
|
);
|
|
146
156
|
|
|
147
157
|
if (added > 0) {
|
|
148
|
-
this.#writeJSON(destPath, existing);
|
|
149
|
-
logger.info(` Updated settings.json (${added} new entries)`);
|
|
158
|
+
await this.#writeJSON(destPath, existing);
|
|
159
|
+
this.#logger.info(` Updated settings.json (${added} new entries)`);
|
|
150
160
|
} else {
|
|
151
|
-
logger.info(` Settings up to date`);
|
|
161
|
+
this.#logger.info(` Settings up to date`);
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
/**
|
|
156
|
-
* Initialize a new knowledge base
|
|
166
|
+
* Initialize a new knowledge base.
|
|
157
167
|
* @param {string} targetPath
|
|
158
168
|
* @param {string} templateDir
|
|
169
|
+
* @returns {Promise<{ok: true, value: {dest: string}} | {ok: false, code: number, error: string}>}
|
|
159
170
|
*/
|
|
160
|
-
init(targetPath, templateDir) {
|
|
171
|
+
async init(targetPath, templateDir) {
|
|
161
172
|
const dest = this.#expandPath(targetPath);
|
|
162
|
-
if (this.#
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
if (await this.#exists(join(dest, "CLAUDE.md"))) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
code: 1,
|
|
177
|
+
error: `Knowledge base already exists at ${dest}`,
|
|
178
|
+
};
|
|
165
179
|
}
|
|
166
180
|
|
|
167
|
-
this.#ensureDir(dest);
|
|
181
|
+
await this.#ensureDir(dest);
|
|
168
182
|
for (const d of [
|
|
169
183
|
"knowledge/People",
|
|
170
184
|
"knowledge/Organizations",
|
|
@@ -172,30 +186,39 @@ export class KBManager {
|
|
|
172
186
|
"knowledge/Topics",
|
|
173
187
|
"knowledge/Briefings",
|
|
174
188
|
])
|
|
175
|
-
this.#ensureDir(join(dest, d));
|
|
189
|
+
await this.#ensureDir(join(dest, d));
|
|
176
190
|
|
|
177
|
-
this.#fs.
|
|
191
|
+
await this.#fs.copyFile(
|
|
192
|
+
join(templateDir, "USER.md"),
|
|
193
|
+
join(dest, "USER.md"),
|
|
194
|
+
);
|
|
178
195
|
|
|
179
|
-
this.copyBundledFiles(templateDir, dest);
|
|
196
|
+
await this.copyBundledFiles(templateDir, dest);
|
|
180
197
|
|
|
181
|
-
logger.info(
|
|
198
|
+
this.#logger.info(
|
|
182
199
|
`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
200
|
);
|
|
201
|
+
return { ok: true, value: { dest } };
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
/**
|
|
187
205
|
* Update an existing knowledge base with the latest bundled files.
|
|
188
206
|
* @param {string} targetPath
|
|
189
207
|
* @param {string} templateDir
|
|
208
|
+
* @returns {Promise<{ok: true, value: {dest: string}} | {ok: false, code: number, error: string}>}
|
|
190
209
|
*/
|
|
191
|
-
update(targetPath, templateDir) {
|
|
210
|
+
async update(targetPath, templateDir) {
|
|
192
211
|
const dest = this.#expandPath(targetPath);
|
|
193
|
-
if (!this.#
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
if (!(await this.#exists(join(dest, "CLAUDE.md")))) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
code: 1,
|
|
216
|
+
error: `No knowledge base found at ${dest}`,
|
|
217
|
+
};
|
|
196
218
|
}
|
|
197
|
-
this.copyBundledFiles(templateDir, dest);
|
|
198
|
-
logger.info(`\nKnowledge base updated: ${dest}`);
|
|
219
|
+
await this.copyBundledFiles(templateDir, dest);
|
|
220
|
+
this.#logger.info(`\nKnowledge base updated: ${dest}`);
|
|
221
|
+
return { ok: true, value: { dest } };
|
|
199
222
|
}
|
|
200
223
|
|
|
201
224
|
/**
|
|
@@ -206,23 +229,3 @@ export class KBManager {
|
|
|
206
229
|
return p.startsWith("~/") ? join(homedir(), p.slice(2)) : resolve(p);
|
|
207
230
|
}
|
|
208
231
|
}
|
|
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
|
-
}
|