@forwardimpact/outpost 3.6.0 → 3.8.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/package.json +1 -1
- package/src/outpost.js +28 -44
- package/src/socket-server.js +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/outpost",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.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": {
|
package/src/outpost.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// fit-outpost daemon Run continuously (poll every 60s)
|
|
6
6
|
// fit-outpost wake <agent> Wake a specific agent immediately
|
|
7
7
|
// fit-outpost init <path> Initialize a new knowledge base
|
|
8
|
-
// fit-outpost update [path] Update KB with latest CLAUDE.md, agents and skills
|
|
8
|
+
// fit-outpost update [path] Update KB with latest CLAUDE.md, agents and skills (defaults to current directory)
|
|
9
9
|
// fit-outpost stop Gracefully stop daemon and all running agents
|
|
10
10
|
// fit-outpost validate Validate agent definitions exist
|
|
11
11
|
// fit-outpost status Show agent status
|
|
@@ -26,7 +26,7 @@ import { StateManager } from "./state-manager.js";
|
|
|
26
26
|
import { AgentRunner } from "./agent-runner.js";
|
|
27
27
|
import { Scheduler, formatLocalTime } from "./scheduler.js";
|
|
28
28
|
import { KBManager } from "./kb-manager.js";
|
|
29
|
-
import { SocketServer, requestShutdown } from "./socket-server.js";
|
|
29
|
+
import { SocketServer, requestShutdown, requestWake } from "./socket-server.js";
|
|
30
30
|
import {
|
|
31
31
|
readPosture,
|
|
32
32
|
writePosture,
|
|
@@ -64,7 +64,8 @@ function buildDefinition(version) {
|
|
|
64
64
|
{
|
|
65
65
|
name: "update",
|
|
66
66
|
args: "[path]",
|
|
67
|
-
description:
|
|
67
|
+
description:
|
|
68
|
+
"Update KB with latest CLAUDE.md, agents and skills (defaults to current directory)",
|
|
68
69
|
},
|
|
69
70
|
{
|
|
70
71
|
name: "stop",
|
|
@@ -302,39 +303,11 @@ export async function run(runtime, version) {
|
|
|
302
303
|
const tpl = await requireTemplateDir();
|
|
303
304
|
if (tpl === null) return 1;
|
|
304
305
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
return 0;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const config = await loadConfig();
|
|
315
|
-
const kbPaths = [
|
|
316
|
-
...new Set(
|
|
317
|
-
Object.values(config.agents)
|
|
318
|
-
.filter((a) => a.kb)
|
|
319
|
-
.map((a) => expandPath(a.kb)),
|
|
320
|
-
),
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
if (kbPaths.length === 0) {
|
|
324
|
-
proc.stderr.write(
|
|
325
|
-
"No knowledge bases configured and no path given.\n" +
|
|
326
|
-
"Usage: fit-outpost update [path]\n",
|
|
327
|
-
);
|
|
328
|
-
return 1;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
for (const kb of kbPaths) {
|
|
332
|
-
logger.info(`\nUpdating ${kb}...`);
|
|
333
|
-
const result = await kbManager.update(kb, tpl);
|
|
334
|
-
if (!result.ok) {
|
|
335
|
-
proc.stderr.write(result.error + "\n");
|
|
336
|
-
return result.code;
|
|
337
|
-
}
|
|
306
|
+
const target = args[0] ?? proc.cwd();
|
|
307
|
+
const result = await kbManager.update(target, tpl);
|
|
308
|
+
if (!result.ok) {
|
|
309
|
+
proc.stderr.write(result.error + "\n");
|
|
310
|
+
return result.code;
|
|
338
311
|
}
|
|
339
312
|
return 0;
|
|
340
313
|
}
|
|
@@ -436,17 +409,28 @@ export async function run(runtime, version) {
|
|
|
436
409
|
cli.usageError("missing required argument <agent>");
|
|
437
410
|
return 2;
|
|
438
411
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
412
|
+
// Always route the wake through the running daemon. The daemon is the
|
|
413
|
+
// only spawn site that descends from fit-outpost.app, so a `claude`
|
|
414
|
+
// spawned there inherits the app as its TCC responsible process and a
|
|
415
|
+
// single grant to the app covers it. Spawning from this CLI process
|
|
416
|
+
// would attribute the access to the terminal instead, breaking the
|
|
417
|
+
// single-grant model. If no daemon is running there is nowhere to wake
|
|
418
|
+
// with correct attribution, so this errors rather than spawning locally.
|
|
419
|
+
const result = await requestWake(SOCKET_PATH, args[0], runtime);
|
|
420
|
+
if (result.ok) {
|
|
421
|
+
log(`Wake dispatched to daemon for "${args[0]}".`);
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
if (result.reason === "not-running") {
|
|
443
425
|
cli.error(
|
|
444
|
-
|
|
426
|
+
"daemon not running. Start fit-outpost.app (or run `fit-outpost daemon`) before waking an agent.",
|
|
445
427
|
);
|
|
446
|
-
|
|
428
|
+
} else if (result.reason === "timeout") {
|
|
429
|
+
cli.error("daemon did not respond to the wake request.");
|
|
430
|
+
} else {
|
|
431
|
+
cli.error(result.message);
|
|
447
432
|
}
|
|
448
|
-
|
|
449
|
-
return 0;
|
|
433
|
+
return 1;
|
|
450
434
|
},
|
|
451
435
|
init: async () => {
|
|
452
436
|
if (!args[0]) {
|
package/src/socket-server.js
CHANGED
|
@@ -323,6 +323,71 @@ export class SocketServer {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Connect to the running daemon and ask it to wake an agent.
|
|
328
|
+
*
|
|
329
|
+
* The wake runs inside the daemon process, which is the only spawn site that
|
|
330
|
+
* descends from fit-outpost.app — so the spawned `claude` inherits the app as
|
|
331
|
+
* its TCC responsible process. Routing every wake through the daemon is what
|
|
332
|
+
* keeps the single-grant model intact; a wake spawned from this CLI process
|
|
333
|
+
* would be attributed to the terminal instead. The daemon acknowledges
|
|
334
|
+
* (`ack`) once it has accepted the request and then runs the wake
|
|
335
|
+
* asynchronously, so this resolves on the ack rather than on completion.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} socketPath
|
|
338
|
+
* @param {string} agent - Agent name to wake.
|
|
339
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
340
|
+
* Injected runtime bag (uses `fsSync` and `clock`).
|
|
341
|
+
* @returns {Promise<{ ok: boolean, reason?: "not-running"|"timeout"|"error", message?: string }>}
|
|
342
|
+
*/
|
|
343
|
+
export async function requestWake(socketPath, agent, runtime) {
|
|
344
|
+
if (!runtime?.fsSync) throw new Error("runtime.fsSync is required");
|
|
345
|
+
if (!runtime?.clock) throw new Error("runtime.clock is required");
|
|
346
|
+
if (!runtime.fsSync.existsSync(socketPath)) {
|
|
347
|
+
return { ok: false, reason: "not-running" };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
const timeout = runtime.clock.setTimeout(() => {
|
|
352
|
+
socket.destroy();
|
|
353
|
+
resolve({ ok: false, reason: "timeout" });
|
|
354
|
+
}, 5000);
|
|
355
|
+
|
|
356
|
+
const socket = createConnection(socketPath, () => {
|
|
357
|
+
socket.write(JSON.stringify({ type: "wake", agent }) + "\n");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
let buffer = "";
|
|
361
|
+
socket.on("data", (data) => {
|
|
362
|
+
buffer += data.toString();
|
|
363
|
+
const idx = buffer.indexOf("\n");
|
|
364
|
+
if (idx === -1) return;
|
|
365
|
+
runtime.clock.clearTimeout(timeout);
|
|
366
|
+
let msg = null;
|
|
367
|
+
try {
|
|
368
|
+
msg = JSON.parse(buffer.slice(0, idx));
|
|
369
|
+
} catch {}
|
|
370
|
+
socket.destroy();
|
|
371
|
+
if (msg && msg.type === "ack" && msg.command === "wake") {
|
|
372
|
+
resolve({ ok: true });
|
|
373
|
+
} else {
|
|
374
|
+
resolve({
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: "error",
|
|
377
|
+
message: msg?.message || "daemon rejected wake request",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// A stale socket file (daemon crashed) refuses the connection; treat it
|
|
383
|
+
// the same as a missing daemon.
|
|
384
|
+
socket.on("error", () => {
|
|
385
|
+
runtime.clock.clearTimeout(timeout);
|
|
386
|
+
resolve({ ok: false, reason: "not-running" });
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
326
391
|
/**
|
|
327
392
|
* Connect to the daemon socket and request graceful shutdown.
|
|
328
393
|
* @param {string} socketPath
|