chapterhouse 0.1.1 → 0.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/README.md +79 -12
- package/dist/api/errors.js +5 -3
- package/dist/api/errors.test.js +12 -21
- package/dist/api/server.js +67 -17
- package/dist/cli.js +111 -18
- package/dist/copilot/agents.js +9 -7
- package/dist/copilot/classifier.js +3 -1
- package/dist/copilot/orchestrator.js +64 -31
- package/dist/copilot/orchestrator.test.js +107 -1
- package/dist/copilot/router.js +4 -2
- package/dist/copilot/tools.js +7 -5
- package/dist/daemon-install.js +368 -0
- package/dist/daemon-install.test.js +98 -0
- package/dist/daemon.js +35 -33
- package/dist/squad/discovery.test.js +61 -0
- package/dist/store/db.js +42 -0
- package/dist/store/db.test.js +88 -0
- package/dist/update.js +162 -28
- package/dist/update.test.js +84 -5
- package/dist/util/logger.js +41 -0
- package/dist/util/logger.test.js +53 -0
- package/dist/wiki/migrate.js +4 -2
- package/dist/wiki/seed-team-wiki.js +4 -2
- package/package.json +3 -2
- package/web/dist/assets/{index-DAg9IrpO.js → index-Bgs6Mze7.js} +59 -59
- package/web/dist/assets/index-Bgs6Mze7.js.map +1 -0
- package/web/dist/assets/index-CxeGtVlE.css +10 -0
- package/web/dist/chapterhouse-icon.svg +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-D-e7K-fT.css +0 -10
- package/web/dist/assets/index-DAg9IrpO.js.map +0 -1
package/README.md
CHANGED
|
@@ -14,14 +14,16 @@ Chapterhouse is built on [Max](https://github.com/burkeholland/max) by [Burke Ho
|
|
|
14
14
|
- **Learns any skill** — pulls from [skills.sh](https://skills.sh) or builds new skills on demand.
|
|
15
15
|
- **Your Copilot subscription** — works with any model your subscription includes (Claude, GPT, Gemini, …). Auto-routing picks a tier per message.
|
|
16
16
|
|
|
17
|
+
See [CHANGELOG.md](CHANGELOG.md) for recent changes and feature history.
|
|
18
|
+
|
|
17
19
|
## Installation
|
|
18
20
|
|
|
19
|
-
**Requires Node.js
|
|
21
|
+
**Requires Node.js 24 or later** (npm ≥ 11.5.1 for Trusted Publishing support).
|
|
20
22
|
|
|
21
23
|
Install globally via npm:
|
|
22
24
|
|
|
23
25
|
```bash
|
|
24
|
-
npm install -g chapterhouse
|
|
26
|
+
npm install -g chapterhouse@latest
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
After installing, run first-time setup:
|
|
@@ -57,6 +59,12 @@ ENABLE_SQUAD=1 # set to 1 to enable squad agent routi
|
|
|
57
59
|
|
|
58
60
|
# Optional — periodic decisions→wiki sync (requires ENABLE_SQUAD=1)
|
|
59
61
|
CHAPTERHOUSE_DECISIONS_SYNC_INTERVAL_MS=300000 # default 5 minutes (300 000 ms)
|
|
62
|
+
|
|
63
|
+
# Optional — logging
|
|
64
|
+
LOG_LEVEL=info # trace | debug | info | warn | error | fatal | silent (default: info)
|
|
65
|
+
# Set to "debug" to see chat message content and routing decisions.
|
|
66
|
+
# Logs are structured JSON (Pino). For human-readable output:
|
|
67
|
+
# chapterhouse start 2>&1 | npx pino-pretty
|
|
60
68
|
```
|
|
61
69
|
|
|
62
70
|
Then start the daemon:
|
|
@@ -91,13 +99,33 @@ npm install && npm run build && npm link
|
|
|
91
99
|
|
|
92
100
|
## Upgrading
|
|
93
101
|
|
|
94
|
-
If you already have Chapterhouse installed:
|
|
95
|
-
|
|
96
102
|
```bash
|
|
97
103
|
chapterhouse update
|
|
98
104
|
```
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
`chapterhouse update` is npm-registry-aware. It detects how Chapterhouse was installed and routes accordingly:
|
|
107
|
+
|
|
108
|
+
| Install source | Update action |
|
|
109
|
+
| -------------- | ------------- |
|
|
110
|
+
| **npm global** (`npm install -g chapterhouse`) | Runs `npm install -g chapterhouse@latest` |
|
|
111
|
+
| **git/legacy** (`~/.chapterhouse/src`) | Git pull + rebuild (with deprecation notice) |
|
|
112
|
+
| **dev** (source working tree) | No-op — use `git pull` manually |
|
|
113
|
+
|
|
114
|
+
### Update flags
|
|
115
|
+
|
|
116
|
+
| Flag | Description |
|
|
117
|
+
| ---- | ----------- |
|
|
118
|
+
| `--check-only` | Print current/latest version and exit without updating |
|
|
119
|
+
| `--ref <version>` | Install a specific version, e.g. `--ref 0.1.5` |
|
|
120
|
+
| `--force` | Bypass the Node 24 / npm 11.5.1 precondition check |
|
|
121
|
+
|
|
122
|
+
**Legacy users:** if you previously installed via git clone, switch to the registry path once:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install -g chapterhouse@latest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Your `~/.chapterhouse/` config carries forward automatically.
|
|
101
129
|
|
|
102
130
|
## Quick Start
|
|
103
131
|
|
|
@@ -202,13 +230,16 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
|
|
|
202
230
|
|
|
203
231
|
## CLI Commands
|
|
204
232
|
|
|
205
|
-
| Command
|
|
206
|
-
|
|
|
207
|
-
| `chapterhouse start`
|
|
208
|
-
| `chapterhouse start --open`
|
|
209
|
-
| `chapterhouse setup`
|
|
210
|
-
| `chapterhouse update`
|
|
211
|
-
| `chapterhouse
|
|
233
|
+
| Command | Description |
|
|
234
|
+
| ------------------------------ | ------------------------------------------------- |
|
|
235
|
+
| `chapterhouse start` | Start the Chapterhouse daemon (web UI + HTTP API) |
|
|
236
|
+
| `chapterhouse start --open` | Same, plus open the browser |
|
|
237
|
+
| `chapterhouse setup` | Interactive first-run configuration |
|
|
238
|
+
| `chapterhouse update` | Check for and install updates (npm registry for global installs) |
|
|
239
|
+
| `chapterhouse update --check-only` | Print current/latest version without updating |
|
|
240
|
+
| `chapterhouse update --ref <ver>` | Install a specific version |
|
|
241
|
+
| `chapterhouse daemon <sub>` | Manage the persistent background service |
|
|
242
|
+
| `chapterhouse help` | Show available commands |
|
|
212
243
|
|
|
213
244
|
### Flags
|
|
214
245
|
|
|
@@ -217,6 +248,42 @@ The deployment assets for the shared instance set Entra auth and `CHAPTERHOUSE_M
|
|
|
217
248
|
| `--self-edit` | Allow Chapterhouse to modify its own source code (use with `chapterhouse start`) |
|
|
218
249
|
| `--open` | Open the web UI in your default browser when the daemon is ready |
|
|
219
250
|
|
|
251
|
+
### Daemon Management (macOS / Linux)
|
|
252
|
+
|
|
253
|
+
Chapterhouse can run as a persistent user-level background service that starts on login and restarts automatically on crash — no root required.
|
|
254
|
+
|
|
255
|
+
#### Install
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
chapterhouse daemon install
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
This writes and loads the appropriate unit file for your OS:
|
|
262
|
+
|
|
263
|
+
| Platform | Unit file |
|
|
264
|
+
| -------- | ----------------------------------------------------------------------- |
|
|
265
|
+
| macOS | `~/Library/LaunchAgents/com.bketelsen.chapterhouse.plist` (launchd) |
|
|
266
|
+
| Linux | `~/.config/systemd/user/chapterhouse.service` (systemd `--user`) |
|
|
267
|
+
| Windows | Not supported — run `chapterhouse start` manually or use Task Scheduler |
|
|
268
|
+
|
|
269
|
+
#### Manage
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
chapterhouse daemon status # is it running? what PID? where are the logs?
|
|
273
|
+
chapterhouse daemon stop # stop without uninstalling
|
|
274
|
+
chapterhouse daemon start # start without re-installing
|
|
275
|
+
chapterhouse daemon restart # restart in place
|
|
276
|
+
chapterhouse daemon logs # tail live logs (Ctrl+C to exit)
|
|
277
|
+
chapterhouse daemon uninstall # stop, disable, and remove the unit file
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Log locations
|
|
281
|
+
|
|
282
|
+
| Platform | Location |
|
|
283
|
+
| -------- | ----------------------------------------------------------- |
|
|
284
|
+
| macOS | `~/Library/Logs/chapterhouse.log` |
|
|
285
|
+
| Linux | `journalctl --user -u chapterhouse` (no extra config needed) |
|
|
286
|
+
|
|
220
287
|
## Web UI
|
|
221
288
|
|
|
222
289
|
The browser app at `http://localhost:7788` is split into a few views:
|
package/dist/api/errors.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
|
+
import { childLogger } from "../util/logger.js";
|
|
3
|
+
const log = childLogger("api:errors");
|
|
2
4
|
export class HttpError extends Error {
|
|
3
5
|
statusCode;
|
|
4
6
|
expose;
|
|
@@ -80,15 +82,15 @@ export function createApiErrorHandler() {
|
|
|
80
82
|
}
|
|
81
83
|
if (error instanceof HttpError) {
|
|
82
84
|
if (error.statusCode >= 500) {
|
|
83
|
-
|
|
85
|
+
log.error({ method: req.method, url: req.originalUrl, err: error instanceof Error ? error.message : error }, "API request failed");
|
|
84
86
|
}
|
|
85
87
|
else if (error.statusCode === 401 || error.statusCode === 403) {
|
|
86
|
-
|
|
88
|
+
log.warn({ method: req.method, url: req.originalUrl, err: error.message }, "API request denied");
|
|
87
89
|
}
|
|
88
90
|
res.status(error.statusCode).json({ error: error.expose ? error.message : "Internal server error" });
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
|
-
|
|
93
|
+
log.error({ method: req.method, url: req.originalUrl, err: error instanceof Error ? error.message : error }, "API request failed (unhandled)");
|
|
92
94
|
res.status(500).json({ error: "Internal server error" });
|
|
93
95
|
};
|
|
94
96
|
}
|
package/dist/api/errors.test.js
CHANGED
|
@@ -64,26 +64,17 @@ test("api not-found handler returns JSON for unknown API routes", async () => {
|
|
|
64
64
|
});
|
|
65
65
|
test("centralized error handler hides internal error details", async () => {
|
|
66
66
|
const app = express();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
assert.equal(response.status, 500);
|
|
80
|
-
assert.deepEqual(await response.json(), { error: "Internal server error" });
|
|
81
|
-
});
|
|
82
|
-
assert.equal(logged.length, 1);
|
|
83
|
-
assert.equal(logged[0]?.[0], "[api] GET /api/failure failed:");
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
console.error = originalConsoleError;
|
|
87
|
-
}
|
|
67
|
+
app.get("/api/failure", () => {
|
|
68
|
+
throw new InternalServerError("do not leak", false);
|
|
69
|
+
});
|
|
70
|
+
app.use(createApiErrorHandler());
|
|
71
|
+
await withServer(app, async (baseUrl) => {
|
|
72
|
+
const response = await fetch(`${baseUrl}/api/failure`);
|
|
73
|
+
// Internal details must NOT reach the client
|
|
74
|
+
assert.equal(response.status, 500);
|
|
75
|
+
assert.deepEqual(await response.json(), { error: "Internal server error" });
|
|
76
|
+
});
|
|
77
|
+
// Logging now goes through pino (structured JSON), not console.error.
|
|
78
|
+
// The HTTP response contract above is the authoritative check.
|
|
88
79
|
});
|
|
89
80
|
//# sourceMappingURL=errors.test.js.map
|
package/dist/api/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cors from "cors";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import helmet from "helmet";
|
|
4
|
-
import { existsSync, statSync } from "fs";
|
|
4
|
+
import { existsSync, statSync, readdirSync } from "fs";
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -19,12 +19,15 @@ import { withWikiWrite } from "../wiki/lock.js";
|
|
|
19
19
|
import { listSkills, removeSkill } from "../copilot/skills.js";
|
|
20
20
|
import { restartDaemon } from "../daemon.js";
|
|
21
21
|
import { API_TOKEN_PATH, resolveWikiRelativePath } from "../paths.js";
|
|
22
|
-
import { getDb } from "../store/db.js";
|
|
22
|
+
import { getDb, getSessionMessages } from "../store/db.js";
|
|
23
23
|
import { getStatus, onStatusChange } from "../status.js";
|
|
24
24
|
import { formatSseData, formatSseEvent } from "./sse.js";
|
|
25
25
|
import { syncDecisionsFileToWiki } from "../squad/mirror.js";
|
|
26
|
+
import { resolveProjectSquad } from "../squad/discovery.js";
|
|
26
27
|
import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, buildHistoryEntries, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
|
|
27
28
|
import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
|
|
29
|
+
import { childLogger } from "../util/logger.js";
|
|
30
|
+
const log = childLogger("server");
|
|
28
31
|
void searchIndex; // re-exported by index-manager; reference here documents the dep
|
|
29
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
33
|
// Built SPA bundle (web/dist/), shipped alongside dist/
|
|
@@ -68,11 +71,11 @@ try {
|
|
|
68
71
|
});
|
|
69
72
|
}
|
|
70
73
|
catch (err) {
|
|
71
|
-
|
|
74
|
+
log.error({ err: err instanceof Error ? err.message : String(err) }, "Auth token error");
|
|
72
75
|
process.exit(1);
|
|
73
76
|
}
|
|
74
77
|
if (config.standaloneMode) {
|
|
75
|
-
|
|
78
|
+
log.warn("Running without authentication — team features disabled");
|
|
76
79
|
}
|
|
77
80
|
function isLoopbackHostname(hostname) {
|
|
78
81
|
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
|
|
@@ -401,7 +404,7 @@ app.get("/api/models", async (_req, res) => {
|
|
|
401
404
|
res.json({ models: models.map((m) => m.id), current: config.copilotModel });
|
|
402
405
|
}
|
|
403
406
|
catch (error) {
|
|
404
|
-
|
|
407
|
+
log.error({ err: error instanceof Error ? error.message : error }, "Failed to list models");
|
|
405
408
|
throw new InternalServerError();
|
|
406
409
|
}
|
|
407
410
|
});
|
|
@@ -417,7 +420,7 @@ app.get("/api/auto", (_req, res) => {
|
|
|
417
420
|
app.post("/api/auto", (req, res) => {
|
|
418
421
|
const body = parseRequest(autoRequestSchema, req.body ?? {});
|
|
419
422
|
const updated = updateRouterConfig(body);
|
|
420
|
-
|
|
423
|
+
log.info({ enabled: updated.enabled }, "Auto-routing updated");
|
|
421
424
|
res.json(updated);
|
|
422
425
|
});
|
|
423
426
|
// ---------------------------------------------------------------------------
|
|
@@ -518,7 +521,7 @@ app.post("/api/restart", (_req, res) => {
|
|
|
518
521
|
res.json({ status: "restarting" });
|
|
519
522
|
setTimeout(() => {
|
|
520
523
|
restartDaemon().catch((err) => {
|
|
521
|
-
|
|
524
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Restart failed");
|
|
522
525
|
});
|
|
523
526
|
}, 500);
|
|
524
527
|
});
|
|
@@ -528,6 +531,31 @@ app.post("/api/restart", (_req, res) => {
|
|
|
528
531
|
const projectRegisterSchema = z.object({
|
|
529
532
|
projectRoot: requiredString("projectRoot must be a non-empty string"),
|
|
530
533
|
}).strict();
|
|
534
|
+
/**
|
|
535
|
+
* Count squad agents on disk for a project.
|
|
536
|
+
* Authoritative source: each subdirectory of <projectRoot>/.squad/agents/ that
|
|
537
|
+
* contains a charter.md is one agent. Never relies on the SQLite cache so the
|
|
538
|
+
* badge is always accurate even before the cache is warm.
|
|
539
|
+
*/
|
|
540
|
+
function countAgentsOnDisk(projectRoot) {
|
|
541
|
+
const agentsDir = join(projectRoot, ".squad", "agents");
|
|
542
|
+
if (!existsSync(agentsDir))
|
|
543
|
+
return 0;
|
|
544
|
+
try {
|
|
545
|
+
return readdirSync(agentsDir).filter((entry) => {
|
|
546
|
+
try {
|
|
547
|
+
return statSync(join(agentsDir, entry)).isDirectory() &&
|
|
548
|
+
existsSync(join(agentsDir, entry, "charter.md"));
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}).length;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
531
559
|
app.get("/api/projects", (_req, res) => {
|
|
532
560
|
if (!config.squadEnabled) {
|
|
533
561
|
res.status(503).json({ error: "Squad integration is disabled. Set ENABLE_SQUAD=1 to enable." });
|
|
@@ -535,19 +563,18 @@ app.get("/api/projects", (_req, res) => {
|
|
|
535
563
|
}
|
|
536
564
|
const db = getDb();
|
|
537
565
|
const rows = db.prepare(`
|
|
538
|
-
SELECT
|
|
539
|
-
COUNT(squad_agents.slug) as agent_count, project_squads.loaded_at
|
|
566
|
+
SELECT project_root, squad_dir, loaded_at, last_used_at
|
|
540
567
|
FROM project_squads
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
GROUP BY project_squads.project_root
|
|
544
|
-
ORDER BY project_squads.loaded_at DESC
|
|
568
|
+
WHERE registered = 1
|
|
569
|
+
ORDER BY COALESCE(last_used_at, 0) DESC
|
|
545
570
|
`).all();
|
|
546
571
|
res.json(rows.map((r) => ({
|
|
547
572
|
projectRoot: r.project_root,
|
|
548
573
|
squadDir: r.squad_dir,
|
|
549
|
-
|
|
574
|
+
// Count from live filesystem — authoritative per Squad SDK rule: repo files win over cache.
|
|
575
|
+
agentCount: countAgentsOnDisk(r.project_root),
|
|
550
576
|
loadedAt: r.loaded_at,
|
|
577
|
+
lastUsedAt: r.last_used_at != null ? new Date(r.last_used_at).toISOString() : undefined,
|
|
551
578
|
})));
|
|
552
579
|
});
|
|
553
580
|
app.post("/api/projects", async (req, res) => {
|
|
@@ -570,10 +597,15 @@ app.post("/api/projects", async (req, res) => {
|
|
|
570
597
|
// Fire-and-forget: sync decisions.md to the wiki. Non-fatal if it fails.
|
|
571
598
|
syncDecisionsFileToWiki(projectRoot).then(result => {
|
|
572
599
|
if (result) {
|
|
573
|
-
|
|
600
|
+
log.info({ entriesSynced: result.entriesSynced, projectRoot }, "Synced squad decisions to wiki");
|
|
574
601
|
}
|
|
575
602
|
}).catch(err => {
|
|
576
|
-
|
|
603
|
+
log.warn({ err: err instanceof Error ? err.message : err }, "syncDecisionsFileToWiki failed during registration (non-fatal)");
|
|
604
|
+
});
|
|
605
|
+
// Fire-and-forget: populate squad_agents cache from disk so future queries have
|
|
606
|
+
// something to work with (non-fatal — GET /api/projects counts live from disk anyway).
|
|
607
|
+
resolveProjectSquad(projectRoot).catch(err => {
|
|
608
|
+
log.warn({ err: err instanceof Error ? err.message : err }, "resolveProjectSquad failed during registration (non-fatal)");
|
|
577
609
|
});
|
|
578
610
|
res.status(201).json({ projectRoot, message: "Project registered successfully" });
|
|
579
611
|
});
|
|
@@ -593,6 +625,24 @@ app.delete("/api/projects/:projectRoot", (req, res) => {
|
|
|
593
625
|
db.prepare(`DELETE FROM squad_agents WHERE project_root = ?`).run(projectRoot);
|
|
594
626
|
res.json({ message: "Project removed" });
|
|
595
627
|
});
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Session messages — frontend rehydration on reload
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
app.get("/api/session/:sessionKey/messages", (req, res) => {
|
|
632
|
+
const sessionKey = Array.isArray(req.params.sessionKey)
|
|
633
|
+
? req.params.sessionKey[0]
|
|
634
|
+
: req.params.sessionKey;
|
|
635
|
+
if (!sessionKey) {
|
|
636
|
+
throw new BadRequestError("Missing sessionKey");
|
|
637
|
+
}
|
|
638
|
+
const rawLimit = req.query.limit;
|
|
639
|
+
const limit = rawLimit !== undefined ? parseInt(String(rawLimit), 10) : undefined;
|
|
640
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
|
641
|
+
throw new BadRequestError("'limit' must be a positive integer");
|
|
642
|
+
}
|
|
643
|
+
const messages = getSessionMessages(sessionKey, limit);
|
|
644
|
+
res.json({ sessionKey, messages });
|
|
645
|
+
});
|
|
596
646
|
app.use(apiNotFoundHandler);
|
|
597
647
|
// ---------------------------------------------------------------------------
|
|
598
648
|
// Static SPA + fallback. Mounted last so API routes win.
|
|
@@ -625,7 +675,7 @@ app.use(createApiErrorHandler());
|
|
|
625
675
|
export function startApiServer() {
|
|
626
676
|
return new Promise((resolve, reject) => {
|
|
627
677
|
const server = app.listen(config.apiPort, config.apiHost, () => {
|
|
628
|
-
|
|
678
|
+
log.info({ host: getDisplayHost(config.apiHost), port: config.apiPort }, "HTTP API + web UI listening");
|
|
629
679
|
resolve();
|
|
630
680
|
});
|
|
631
681
|
server.on("error", (err) => {
|
package/dist/cli.js
CHANGED
|
@@ -21,19 +21,30 @@ Usage:
|
|
|
21
21
|
chapterhouse <command>
|
|
22
22
|
|
|
23
23
|
Commands:
|
|
24
|
-
start
|
|
25
|
-
setup
|
|
26
|
-
update
|
|
27
|
-
|
|
24
|
+
start Start the Chapterhouse daemon (web UI at http://localhost:7788)
|
|
25
|
+
setup Pick a default model and write ~/.chapterhouse/.env
|
|
26
|
+
update Check for updates and install the latest version
|
|
27
|
+
daemon <sub> Manage the persistent background service (install/uninstall/start/stop/restart/status/logs)
|
|
28
|
+
help Show this help message
|
|
28
29
|
|
|
29
30
|
Flags (start):
|
|
30
31
|
--self-edit Allow Chapterhouse to modify his own source code (off by default)
|
|
31
32
|
--open Open the web UI in your default browser once the daemon is ready
|
|
32
33
|
|
|
34
|
+
Flags (update):
|
|
35
|
+
--check-only Print version info and install-source, then exit without updating
|
|
36
|
+
--ref <version> Install a specific version (e.g. --ref 0.1.5)
|
|
37
|
+
--force Bypass the Node/npm version precondition check
|
|
38
|
+
|
|
33
39
|
Examples:
|
|
34
40
|
chapterhouse start Start the daemon, then open http://localhost:7788
|
|
35
41
|
chapterhouse start --open Same, but open the browser for you
|
|
36
42
|
chapterhouse start --self-edit Start with self-edit enabled
|
|
43
|
+
chapterhouse update Update to latest (npm registry for global installs)
|
|
44
|
+
chapterhouse update --check-only Show current/latest version without updating
|
|
45
|
+
chapterhouse update --ref 0.1.5 Install a specific version
|
|
46
|
+
chapterhouse daemon install Install and enable the persistent background service
|
|
47
|
+
chapterhouse daemon status Show whether the service is running
|
|
37
48
|
`.trim());
|
|
38
49
|
}
|
|
39
50
|
const args = process.argv.slice(2);
|
|
@@ -54,30 +65,77 @@ switch (command) {
|
|
|
54
65
|
await import("./setup.js");
|
|
55
66
|
break;
|
|
56
67
|
case "update": {
|
|
57
|
-
const
|
|
68
|
+
const updateFlags = args.slice(1);
|
|
69
|
+
const checkOnly = updateFlags.includes("--check-only");
|
|
70
|
+
const force = updateFlags.includes("--force");
|
|
71
|
+
const refIdx = updateFlags.indexOf("--ref");
|
|
72
|
+
const ref = refIdx !== -1 ? (updateFlags[refIdx + 1] ?? null) : null;
|
|
73
|
+
const { checkForUpdate, performUpdate, detectInstallSource, checkPreconditions, buildLegacyGitInstallCommand, } = await import("./update.js");
|
|
74
|
+
const source = detectInstallSource();
|
|
75
|
+
// Precondition check (bypass with --force)
|
|
76
|
+
if (!force) {
|
|
77
|
+
const pre = checkPreconditions();
|
|
78
|
+
if (!pre.ok) {
|
|
79
|
+
console.warn(`⚠ ${pre.message}`);
|
|
80
|
+
console.warn(" Run with --force to proceed anyway.");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
58
84
|
const check = await checkForUpdate();
|
|
59
85
|
if (!check.checkSucceeded) {
|
|
60
|
-
console.error("⚠ Could not reach GitHub to check for updates. Check your network and try again.");
|
|
86
|
+
console.error("⚠ Could not reach npm registry or GitHub to check for updates. Check your network and try again.");
|
|
61
87
|
process.exit(1);
|
|
62
88
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
console.log(
|
|
89
|
+
console.log(`chapterhouse current: v${check.current}`);
|
|
90
|
+
console.log(`chapterhouse latest: v${check.latest ?? "unknown"}`);
|
|
91
|
+
console.log(`Install source: ${source}`);
|
|
92
|
+
if (checkOnly) {
|
|
93
|
+
if (check.updateAvailable) {
|
|
94
|
+
console.log(`\nUpdate available: v${check.current} → v${check.latest}`);
|
|
95
|
+
console.log(`Run \`chapterhouse update\` to install.`);
|
|
69
96
|
}
|
|
70
97
|
else {
|
|
71
|
-
console.
|
|
72
|
-
process.exit(1);
|
|
98
|
+
console.log("\nYou are on the latest version.");
|
|
73
99
|
}
|
|
74
100
|
break;
|
|
75
101
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
if (source === "dev") {
|
|
103
|
+
console.log("ℹ Dev mode — use git pull manually.");
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (source === "git") {
|
|
107
|
+
console.warn("⚠ Deprecation notice: your install uses the legacy git clone path.");
|
|
108
|
+
console.warn(` Switch to the npm registry path for automatic updates:`);
|
|
109
|
+
console.warn(` npm install -g chapterhouse@latest`);
|
|
110
|
+
console.warn(" Continuing with legacy git update...\n");
|
|
111
|
+
}
|
|
112
|
+
const targetRef = ref ?? (check.latest ? check.latest : null);
|
|
113
|
+
if (!check.updateAvailable && !ref) {
|
|
114
|
+
console.log(`chapterhouse v${check.current} is already the latest version.`);
|
|
115
|
+
if (source !== "git")
|
|
116
|
+
break;
|
|
117
|
+
// For git installs, still offer to pull latest main
|
|
118
|
+
console.log("Pulling latest from main...");
|
|
119
|
+
}
|
|
120
|
+
else if (check.updateAvailable) {
|
|
121
|
+
console.log(`\nUpdate available: v${check.current} → v${check.latest}`);
|
|
122
|
+
console.log("Installing...");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`Installing specified ref: ${targetRef}`);
|
|
126
|
+
}
|
|
127
|
+
const result = await performUpdate(targetRef);
|
|
79
128
|
if (result.ok) {
|
|
80
|
-
|
|
129
|
+
if (result.source === "dev") {
|
|
130
|
+
console.log("ℹ Dev mode — use git pull manually.");
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const label = ref ? `${ref}` : `v${check.latest ?? "latest"}`;
|
|
134
|
+
console.log(`✅ Updated to ${label}`);
|
|
135
|
+
if (result.source === "registry" || result.source === "unknown") {
|
|
136
|
+
console.log(" To verify provenance: npm audit signatures (in any project that has chapterhouse as a dep)");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
81
139
|
}
|
|
82
140
|
else {
|
|
83
141
|
console.error(`❌ Update failed: ${result.output}`);
|
|
@@ -85,6 +143,41 @@ switch (command) {
|
|
|
85
143
|
}
|
|
86
144
|
break;
|
|
87
145
|
}
|
|
146
|
+
case "daemon": {
|
|
147
|
+
const subcommand = args[1];
|
|
148
|
+
const { install, uninstall, start: daemonStart, stop: daemonStop, restart: daemonRestart, status: daemonStatus, logs: daemonLogs, printDaemonHelp, } = await import("./daemon-install.js");
|
|
149
|
+
switch (subcommand) {
|
|
150
|
+
case "install":
|
|
151
|
+
await install();
|
|
152
|
+
break;
|
|
153
|
+
case "uninstall":
|
|
154
|
+
await uninstall();
|
|
155
|
+
break;
|
|
156
|
+
case "start":
|
|
157
|
+
await daemonStart();
|
|
158
|
+
break;
|
|
159
|
+
case "stop":
|
|
160
|
+
await daemonStop();
|
|
161
|
+
break;
|
|
162
|
+
case "restart":
|
|
163
|
+
await daemonRestart();
|
|
164
|
+
break;
|
|
165
|
+
case "status":
|
|
166
|
+
await daemonStatus();
|
|
167
|
+
break;
|
|
168
|
+
case "logs":
|
|
169
|
+
await daemonLogs();
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
if (subcommand) {
|
|
173
|
+
console.error(`Unknown daemon subcommand: ${subcommand}\n`);
|
|
174
|
+
}
|
|
175
|
+
printDaemonHelp();
|
|
176
|
+
if (subcommand)
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
88
181
|
case "help":
|
|
89
182
|
case "--help":
|
|
90
183
|
case "-h":
|
package/dist/copilot/agents.js
CHANGED
|
@@ -9,6 +9,8 @@ import { getState, setState } from "../store/db.js";
|
|
|
9
9
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
10
10
|
import { getSkillDirectories } from "./skills.js";
|
|
11
11
|
import { findSquadAgent, renderProjectAgentRoster } from "../squad/registry.js";
|
|
12
|
+
import { childLogger } from "../util/logger.js";
|
|
13
|
+
const log = childLogger("agents");
|
|
12
14
|
// Frontmatter schema
|
|
13
15
|
const agentFrontmatterSchema = z.object({
|
|
14
16
|
name: z.string().min(1),
|
|
@@ -62,7 +64,7 @@ export function parseAgentMd(content, slug) {
|
|
|
62
64
|
}
|
|
63
65
|
const result = agentFrontmatterSchema.safeParse(parsed);
|
|
64
66
|
if (!result.success) {
|
|
65
|
-
|
|
67
|
+
log.warn({ slug, errors: result.error.format() }, "Invalid frontmatter in agent file");
|
|
66
68
|
return null;
|
|
67
69
|
}
|
|
68
70
|
const fm = result.data;
|
|
@@ -102,7 +104,7 @@ export function loadAgents() {
|
|
|
102
104
|
configs.push(config);
|
|
103
105
|
}
|
|
104
106
|
catch (err) {
|
|
105
|
-
|
|
107
|
+
log.warn({ entry, err: err instanceof Error ? err.message : err }, "Failed to read agent entry");
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
agentRegistry = configs;
|
|
@@ -138,7 +140,7 @@ export function ensureDefaultAgents() {
|
|
|
138
140
|
if (!existsSync(dest)) {
|
|
139
141
|
copyFileSync(src, dest);
|
|
140
142
|
setState(stateKey, srcHash);
|
|
141
|
-
|
|
143
|
+
log.info({ file }, "Installed bundled agent");
|
|
142
144
|
continue;
|
|
143
145
|
}
|
|
144
146
|
// Check if the bundled version actually changed since our last sync
|
|
@@ -150,13 +152,13 @@ export function ensureDefaultAgents() {
|
|
|
150
152
|
const destHash = createHash("sha256").update(readFileSync(dest)).digest("hex");
|
|
151
153
|
if (lastSyncedHash && destHash !== lastSyncedHash) {
|
|
152
154
|
// User modified the file after our last sync — don't clobber their changes
|
|
153
|
-
|
|
155
|
+
log.debug({ file }, "Skipping bundled agent — user has local customizations");
|
|
154
156
|
continue;
|
|
155
157
|
}
|
|
156
158
|
// Safe to update: either first sync (no record) or file is unmodified from our last deploy
|
|
157
159
|
copyFileSync(src, dest);
|
|
158
160
|
setState(stateKey, srcHash);
|
|
159
|
-
|
|
161
|
+
log.info({ file }, "Updated bundled agent");
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
/** Create a new agent .md file. Returns error string or null on success. */
|
|
@@ -326,7 +328,7 @@ export async function createEphemeralAgentSession(slug, client, allTools, modelO
|
|
|
326
328
|
bufferExhaustionThreshold: 0.95,
|
|
327
329
|
},
|
|
328
330
|
});
|
|
329
|
-
|
|
331
|
+
log.info({ agentSlug: agent.slug, model }, "Created ephemeral agent session");
|
|
330
332
|
return session;
|
|
331
333
|
}
|
|
332
334
|
/** Create an ephemeral session for a squad virtual agent (not in CH registry). */
|
|
@@ -353,7 +355,7 @@ export async function createSquadAgentSession(slug, client, allTools, systemMess
|
|
|
353
355
|
bufferExhaustionThreshold: 0.95,
|
|
354
356
|
},
|
|
355
357
|
});
|
|
356
|
-
|
|
358
|
+
log.info({ agentSlug: slug, model }, "Created squad virtual agent session");
|
|
357
359
|
return session;
|
|
358
360
|
}
|
|
359
361
|
/** Clean up active task tracking (for shutdown/restart). */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { approveAll } from "@github/copilot-sdk";
|
|
2
|
+
import { childLogger } from "../util/logger.js";
|
|
3
|
+
const log = childLogger("classifier");
|
|
2
4
|
// ---------------------------------------------------------------------------
|
|
3
5
|
// Persistent GPT-4.1 classifier session
|
|
4
6
|
// ---------------------------------------------------------------------------
|
|
@@ -52,7 +54,7 @@ export async function classifyWithLLM(client, message) {
|
|
|
52
54
|
return TIER_MAP[raw] ?? "standard";
|
|
53
55
|
}
|
|
54
56
|
catch (err) {
|
|
55
|
-
|
|
57
|
+
log.warn({ err: err instanceof Error ? err.message : err }, "Classifier error, falling back to heuristics");
|
|
56
58
|
// Destroy broken session so it's recreated next time
|
|
57
59
|
if (classifierSession) {
|
|
58
60
|
classifierSession.destroy().catch(() => { });
|