forge-cw 0.1.8 → 0.1.10

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Forge Console</title>
7
- <script type="module" crossorigin src="/assets/index-BpkdtYRb.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-5ifgSOpU.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-N7b3C-h8.css">
9
9
  </head>
10
10
  <body>
package/dist/index.js CHANGED
@@ -656,7 +656,7 @@ var init_cw_reader = __esm({
656
656
  if (!match)
657
657
  return { frontmatter: {}, body: content };
658
658
  const rawBlock = match[1];
659
- const body = match[2];
659
+ const body2 = match[2];
660
660
  const frontmatter = {};
661
661
  let inMetadata = false;
662
662
  const metadataObj = {};
@@ -680,7 +680,7 @@ var init_cw_reader = __esm({
680
680
  }
681
681
  if (Object.keys(metadataObj).length > 0)
682
682
  frontmatter["metadata"] = metadataObj;
683
- return { frontmatter, body };
683
+ return { frontmatter, body: body2 };
684
684
  }
685
685
  // directory whose skills/ holds a scope's skills; an unregistered project ref is used as a path
686
686
  getSkillConfigDir(scope, scopeRef) {
@@ -742,7 +742,7 @@ var init_cw_reader = __esm({
742
742
  return null;
743
743
  try {
744
744
  const content = readFileSync2(skillMd, "utf-8");
745
- const { frontmatter, body } = this.parseFrontmatter(content);
745
+ const { frontmatter, body: body2 } = this.parseFrontmatter(content);
746
746
  const references = [];
747
747
  const refsDir = join2(dir, "references");
748
748
  if (existsSync2(refsDir)) {
@@ -755,7 +755,7 @@ var init_cw_reader = __esm({
755
755
  }
756
756
  }
757
757
  }
758
- return { name: String(frontmatter["name"] ?? name), scope, scopeRef, frontmatter, body, references };
758
+ return { name: String(frontmatter["name"] ?? name), scope, scopeRef, frontmatter, body: body2, references };
759
759
  } catch {
760
760
  return null;
761
761
  }
@@ -814,6 +814,14 @@ var init_cw_types = __esm({
814
814
  import { execFile } from "node:child_process";
815
815
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
816
816
  import { join as join3 } from "node:path";
817
+ function envWithoutHost(env = process.env) {
818
+ const out = {};
819
+ for (const [key, value] of Object.entries(env)) {
820
+ if (value !== void 0 && key !== "CLAUDECODE" && !key.startsWith("CLAUDE_CODE_"))
821
+ out[key] = value;
822
+ }
823
+ return out;
824
+ }
817
825
  function envWithoutHarness(env = process.env) {
818
826
  const out = {};
819
827
  for (const [key, value] of Object.entries(env)) {
@@ -940,8 +948,8 @@ function closeWarningsFor(input) {
940
948
  return warnings;
941
949
  }
942
950
  function createRunner(env, timeoutMs = COMMAND_TIMEOUT_MS) {
943
- return (bin, args, cwd) => new Promise((resolve5) => {
944
- execFile2(bin, args, { cwd, env, timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024 }, (err, stdout, stderr) => {
951
+ return (bin, args, cwd, input) => new Promise((resolve5) => {
952
+ const child = execFile2(bin, args, { cwd, env, timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024 }, (err, stdout, stderr) => {
945
953
  if (!err)
946
954
  return resolve5({ code: 0, stdout: String(stdout), stderr: String(stderr) });
947
955
  const { code } = err;
@@ -949,6 +957,11 @@ function createRunner(env, timeoutMs = COMMAND_TIMEOUT_MS) {
949
957
  return resolve5({ code: 127, stdout: "", stderr: "ENOENT" });
950
958
  resolve5({ code: typeof code === "number" ? code : 1, stdout: String(stdout), stderr: String(stderr) || err.message });
951
959
  });
960
+ if (input !== void 0) {
961
+ child.stdin?.on("error", () => {
962
+ });
963
+ child.stdin?.end(input);
964
+ }
952
965
  });
953
966
  }
954
967
  function isCwLink(worktree, porcelainLine) {
@@ -1298,10 +1311,10 @@ async function probeClaudeUsage(configDir, deps = {}) {
1298
1311
  });
1299
1312
  if (response.status === 401 || response.status === 403)
1300
1313
  return { state: "expired", windows: [], detail: "Claude rejected the stored token" };
1301
- const body = await response.text();
1314
+ const body2 = await response.text();
1302
1315
  if (!response.ok)
1303
1316
  return { state: "error", windows: [], detail: `Claude answered ${response.status}` };
1304
- return { state: "ok", windows: mapClaudeUsage(JSON.parse(body)), detail: null };
1317
+ return { state: "ok", windows: mapClaudeUsage(JSON.parse(body2)), detail: null };
1305
1318
  } catch (err) {
1306
1319
  return { state: "error", windows: [], detail: err instanceof Error ? err.message : String(err) };
1307
1320
  }
@@ -1526,9 +1539,9 @@ var init_login_manager = __esm({
1526
1539
  cwBin;
1527
1540
  spawn;
1528
1541
  entries = /* @__PURE__ */ new Map();
1529
- constructor(cwBin, spawn8 = spawnInPty) {
1542
+ constructor(cwBin, spawn9 = spawnInPty) {
1530
1543
  this.cwBin = cwBin;
1531
- this.spawn = spawn8;
1544
+ this.spawn = spawn9;
1532
1545
  }
1533
1546
  start(account, harness) {
1534
1547
  const key = `${account}::${harness}`;
@@ -1812,14 +1825,14 @@ function cwRoutes(reader, options = {}) {
1812
1825
  return c.json(reader.getAccounts());
1813
1826
  });
1814
1827
  app.post("/accounts", async (c) => {
1815
- const body = await c.req.json();
1816
- const harness = body.harness || void 0;
1817
- const provider = body.provider || void 0;
1818
- const model = body.model || void 0;
1819
- if (!body.name || !body.name.trim()) {
1828
+ const body2 = await c.req.json();
1829
+ const harness = body2.harness || void 0;
1830
+ const provider = body2.provider || void 0;
1831
+ const model = body2.model || void 0;
1832
+ if (!body2.name || !body2.name.trim()) {
1820
1833
  return c.json({ ok: false, error: "Account name is required" }, 400);
1821
1834
  }
1822
- const trimmed = body.name.trim();
1835
+ const trimmed = body2.name.trim();
1823
1836
  if (!ACCOUNT_NAME_RE.test(trimmed)) {
1824
1837
  return c.json({ ok: false, error: "Name must start with a letter or number and contain only letters, numbers, hyphens, and underscores (max 64 chars)" }, 400);
1825
1838
  }
@@ -2883,11 +2896,11 @@ function skillRoutes(reader, options = {}) {
2883
2896
  });
2884
2897
  app.get("/marketplaces", (c) => c.json(listMarketplaces(reader)));
2885
2898
  app.post("/marketplaces", async (c) => {
2886
- const body = await c.req.json().catch(() => ({}));
2887
- const source = body.source?.trim() ?? "";
2899
+ const body2 = await c.req.json().catch(() => ({}));
2900
+ const source = body2.source?.trim() ?? "";
2888
2901
  if (!source || source.startsWith("-"))
2889
2902
  return c.json({ error: "A GitHub repo, URL or path is required" }, 400);
2890
- const account = knownAccount(body.account);
2903
+ const account = knownAccount(body2.account);
2891
2904
  if (!account)
2892
2905
  return c.json({ error: "Unknown account" }, 404);
2893
2906
  const failed = await runClaude("account", account, [["plugin", "marketplace", "add", source]], `Failed to add ${source}`);
@@ -2896,11 +2909,11 @@ function skillRoutes(reader, options = {}) {
2896
2909
  return c.json({ ok: true });
2897
2910
  });
2898
2911
  app.post("/plugins/install", async (c) => {
2899
- const body = await c.req.json().catch(() => ({}));
2900
- const id = body.id ?? "";
2912
+ const body2 = await c.req.json().catch(() => ({}));
2913
+ const id = body2.id ?? "";
2901
2914
  if (!PLUGIN_ID_RE.test(id))
2902
2915
  return c.json({ error: "Invalid plugin id" }, 400);
2903
- const account = knownAccount(body.account);
2916
+ const account = knownAccount(body2.account);
2904
2917
  if (!account)
2905
2918
  return c.json({ error: "Unknown account" }, 404);
2906
2919
  const { name, marketplace } = splitPluginId(id);
@@ -3144,6 +3157,15 @@ var init_pty_manager = __esm({
3144
3157
  get(sessionId) {
3145
3158
  return this.sessions.get(sessionId);
3146
3159
  }
3160
+ // types a line into a live session, then Enter apart, so the agent's prompt takes it as typed and not as a paste
3161
+ typeLine(project, sessionDir, text) {
3162
+ const session = this.sessions.get(this.makeKey(project, sessionDir));
3163
+ if (!session)
3164
+ return false;
3165
+ session.pty.write(text);
3166
+ setTimeout(() => session.pty.write("\r"), 150);
3167
+ return true;
3168
+ }
3147
3169
  // the state tracker reads the screen at the size the user sees it
3148
3170
  resize(sessionId, cols, rows) {
3149
3171
  const session = this.sessions.get(sessionId);
@@ -3237,7 +3259,7 @@ function createJevClassifier({ apiKey, model = "jev-latest", timeoutMs = 3e3, fe
3237
3259
  async classify(input) {
3238
3260
  if (input.exitCode !== null)
3239
3261
  return { state: "exited", confidence: 1, source: "jev" };
3240
- const body = {
3262
+ const body2 = {
3241
3263
  model,
3242
3264
  state: { harness: input.harness, seconds_since_last_output: Math.round(input.quietMs / 1e3), screen: jevScreen(input.screen ?? input.text) },
3243
3265
  questions: {
@@ -3251,7 +3273,7 @@ function createJevClassifier({ apiKey, model = "jev-latest", timeoutMs = 3e3, fe
3251
3273
  const res = await fetch2(JEV_URL, {
3252
3274
  method: "POST",
3253
3275
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3254
- body: JSON.stringify(body),
3276
+ body: JSON.stringify(body2),
3255
3277
  signal: AbortSignal.timeout(timeoutMs)
3256
3278
  });
3257
3279
  if (!res.ok)
@@ -4070,10 +4092,38 @@ import { existsSync as existsSync12 } from "node:fs";
4070
4092
  import { homedir as homedir5 } from "node:os";
4071
4093
  import { join as join13 } from "node:path";
4072
4094
  import { createInterface } from "node:readline";
4095
+ import * as pty3 from "node-pty";
4096
+ function assertRepo(repo, what) {
4097
+ if (!REPO.test(repo))
4098
+ throw new KiokuError(`${what} is named as owner/repo, not "${repo}"`, 400);
4099
+ }
4073
4100
  function resolveKiokuBin(pathVar = process.env.PATH, home = homedir5()) {
4074
4101
  const fallback = join13(home, ".local", "bin", "kioku");
4075
4102
  return findOnPath("kioku", pathVar) ?? (existsSync12(fallback) ? fallback : "kioku");
4076
4103
  }
4104
+ function rememberPrompt(text) {
4105
+ return `Remember this with kioku: ${text.replace(/\s+/g, " ").trim()}`;
4106
+ }
4107
+ function createTerminalRunner(env, timeoutMs) {
4108
+ return (bin, args, cwd) => new Promise((resolve5) => {
4109
+ let proc;
4110
+ try {
4111
+ proc = pty3.spawn(bin, args, { name: "xterm-256color", cols: 400, rows: 40, cwd, env });
4112
+ } catch {
4113
+ return resolve5({ code: 127, stdout: "", stderr: "ENOENT" });
4114
+ }
4115
+ let output = "";
4116
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
4117
+ proc.onData((data) => {
4118
+ output = (output + data).slice(-64 * 1024);
4119
+ });
4120
+ proc.onExit(({ exitCode }) => {
4121
+ clearTimeout(timer);
4122
+ const json = stripAnsi(output).split(/\r?\n/).reverse().map((line) => line.slice(line.indexOf("{")).trim()).find((line) => parseJson(line) !== null);
4123
+ resolve5({ code: exitCode, stdout: json ?? "", stderr: output });
4124
+ });
4125
+ });
4126
+ }
4077
4127
  function parseJson(text) {
4078
4128
  try {
4079
4129
  return JSON.parse(text);
@@ -4093,13 +4143,22 @@ async function mapLimit(items, limit, fn) {
4093
4143
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
4094
4144
  return out;
4095
4145
  }
4096
- var KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, CACHE_MS2, ACTIVITY_SINCE, ACTIVITY_LIMIT, PARALLEL, MISSING, brainName, Kioku;
4146
+ var PAGE_TYPES, KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, WRITE_TIMEOUT_MS, PUBLISH_TIMEOUT_MS, CACHE_MS2, ACTIVITY_SINCE, ACTIVITY_LIMIT, PARALLEL, TIERS, REPO, AREA, LANGUAGE, MISSING, brainName, Kioku;
4097
4147
  var init_kioku = __esm({
4098
4148
  "../core/dist/kioku.js"() {
4099
4149
  "use strict";
4100
4150
  init_task_review();
4151
+ init_cw_doctor();
4101
4152
  init_cw_install();
4102
4153
  init_output();
4154
+ PAGE_TYPES = {
4155
+ memory: { kind: "memory" },
4156
+ instinct: { kind: "memory", sub: "instinct" },
4157
+ decision: { kind: "knowledge", sub: "decision" },
4158
+ playbook: { kind: "knowledge", sub: "playbook" },
4159
+ context: { kind: "knowledge", sub: "context" },
4160
+ rule: { kind: "rule" }
4161
+ };
4103
4162
  KiokuError = class extends Error {
4104
4163
  status;
4105
4164
  constructor(message, status = 502) {
@@ -4109,14 +4168,22 @@ var init_kioku = __esm({
4109
4168
  };
4110
4169
  NOT_INSTALLED = "kioku is not installed on this machine";
4111
4170
  START_TIMEOUT_MS = 2e4;
4171
+ WRITE_TIMEOUT_MS = 6e4;
4172
+ PUBLISH_TIMEOUT_MS = 3 * 6e4;
4112
4173
  CACHE_MS2 = 5 * 6e4;
4113
4174
  ACTIVITY_SINCE = "2w";
4114
4175
  ACTIVITY_LIMIT = 40;
4115
4176
  PARALLEL = 6;
4177
+ TIERS = ["personal", "project", "team"];
4178
+ REPO = /^[\w.-]+\/[\w.-]+(\/[\w.-]+)?$/;
4179
+ AREA = /^[A-Za-z0-9][\w.-]*(\/[A-Za-z0-9][\w.-]*)*$/;
4180
+ LANGUAGE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]+)*$/;
4116
4181
  MISSING = { installed: false, version: null, brains: [], projects: {}, activity: [] };
4117
4182
  brainName = (b) => b.name ?? b.key;
4118
4183
  Kioku = class {
4119
4184
  run;
4185
+ write;
4186
+ terminal;
4120
4187
  startTimeoutMs;
4121
4188
  projects;
4122
4189
  now;
@@ -4126,6 +4193,8 @@ var init_kioku = __esm({
4126
4193
  constructor(options = {}) {
4127
4194
  this.binPath = options.bin;
4128
4195
  this.run = options.run ?? createRunner();
4196
+ this.write = options.write ?? createRunner(envWithoutHost(), WRITE_TIMEOUT_MS);
4197
+ this.terminal = options.terminal;
4129
4198
  this.startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
4130
4199
  this.projects = options.projects ?? (() => ({}));
4131
4200
  this.now = options.now ?? Date.now;
@@ -4163,17 +4232,18 @@ var init_kioku = __esm({
4163
4232
  const entries = Object.entries(this.projects()).filter(([, p]) => existsSync12(p.path));
4164
4233
  const readBrain = async (b) => {
4165
4234
  if (!b.onDisk)
4166
- return { kinds: {}, writes: [] };
4235
+ return { kinds: {}, namespaces: [], writes: [] };
4167
4236
  const [ns, log] = await Promise.all([
4168
4237
  this.json(["namespace", "list", "--brain", b.key], b.folder),
4169
4238
  this.json(["brain", "log", "--since", ACTIVITY_SINCE, "--brain", b.key], b.folder)
4170
4239
  ]);
4171
4240
  const kinds = {};
4172
- for (const n of ns?.brains?.find((x) => x.brain === brainName(b))?.namespaces ?? []) {
4241
+ const namespaces = ns?.brains?.find((x) => x.brain === brainName(b))?.namespaces ?? [];
4242
+ for (const n of namespaces) {
4173
4243
  for (const [kind, count] of Object.entries(n.pages))
4174
4244
  kinds[kind] = (kinds[kind] ?? 0) + count;
4175
4245
  }
4176
- return { kinds, writes: log?.writes ?? [] };
4246
+ return { kinds, namespaces: namespaces.map((n) => n.namespace), writes: log?.writes ?? [] };
4177
4247
  };
4178
4248
  const readChain = async (path) => (await this.json(["brain", "list"], path))?.chain ?? [];
4179
4249
  const [perBrain, chains] = await Promise.all([
@@ -4182,12 +4252,13 @@ var init_kioku = __esm({
4182
4252
  ]);
4183
4253
  const activity = perBrain.flatMap((p) => p.writes).sort((x, y) => y.date.localeCompare(x.date));
4184
4254
  const brains = listed.map((b, i) => {
4185
- const { kinds } = perBrain[i];
4255
+ const { kinds, namespaces } = perBrain[i];
4186
4256
  const name = brainName(b);
4187
4257
  return {
4188
4258
  ...b,
4189
4259
  pages: Object.values(kinds).reduce((sum, n) => sum + n, 0),
4190
4260
  kinds,
4261
+ namespaces,
4191
4262
  touched: activity.find((w) => w.brain === name)?.date ?? null
4192
4263
  };
4193
4264
  });
@@ -4200,6 +4271,133 @@ var init_kioku = __esm({
4200
4271
  activity: activity.slice(0, ACTIVITY_LIMIT).map(({ verb, title, path, date, author, brain }) => ({ verb, title, path, date, author, brain }))
4201
4272
  };
4202
4273
  }
4274
+ // writes through kioku, which validates, commits and pushes a shared brain; Forge never writes a brain's files
4275
+ async save(input) {
4276
+ const type = PAGE_TYPES[input.type];
4277
+ if (!type)
4278
+ throw new KiokuError(`unknown page type ${input.type}`, 400);
4279
+ const brain = await this.findBrain(input.brain);
4280
+ const args = [
4281
+ "page",
4282
+ "save",
4283
+ "--brain",
4284
+ brain.key,
4285
+ "--kind",
4286
+ type.kind,
4287
+ ...type.sub ? ["--sub", type.sub] : [],
4288
+ "--title",
4289
+ input.title,
4290
+ "--summary",
4291
+ input.summary,
4292
+ "--source",
4293
+ "Forge",
4294
+ ...input.namespace ? ["--namespace", input.namespace] : [],
4295
+ ...input.create ? ["--create"] : [],
4296
+ "--json"
4297
+ ];
4298
+ const { out, fail } = await this.invoke(args, brain.folder, this.write, input.body);
4299
+ if (out?.status === "done" && out.path) {
4300
+ this.expire();
4301
+ return { status: "done", brain: brainName(brain), path: out.path, publication: out.publication ?? null };
4302
+ }
4303
+ if (out?.status === "similar")
4304
+ return { status: "similar", similar: (out.similar ?? []).map(({ brain: brain2, path, title, summary }) => ({ brain: brain2, path, title, summary })) };
4305
+ if (out?.status === "namespace")
4306
+ return { status: "namespace", namespaces: (out.namespaces ?? []).map(({ namespace, summary }) => ({ namespace, summary })) };
4307
+ throw fail("kioku could not save the page");
4308
+ }
4309
+ // the cached reading when there is one, else only the list of brains
4310
+ async listBrains() {
4311
+ return this.snapshot ? (await this.snapshot.value).brains : (await this.json(["brain", "list"], homedir5()))?.brains ?? [];
4312
+ }
4313
+ async findBrain(key) {
4314
+ const brain = (await this.listBrains()).find((b) => b.key === key);
4315
+ if (!brain?.onDisk)
4316
+ throw new KiokuError(`no brain ${key} on this machine`, 404);
4317
+ return brain;
4318
+ }
4319
+ // the next status reads again, while the old reading still answers a poll that is not fresh
4320
+ expire() {
4321
+ if (this.snapshot)
4322
+ this.snapshot.at = -Infinity;
4323
+ }
4324
+ // runs a verb that writes; fail turns kioku's refusal into a 400 with its message
4325
+ async invoke(args, cwd, run, input) {
4326
+ const result2 = await run(this.bin, args, cwd, input);
4327
+ if (result2.code === 127)
4328
+ throw new KiokuError(NOT_INSTALLED, 404);
4329
+ const out = parseJson(result2.stdout);
4330
+ return { out, ok: result2.code === 0 && out !== null && !out.error, fail: (fallback) => new KiokuError(out?.error?.message ?? (tailOutput(result2.stderr, 1) || fallback), 400) };
4331
+ }
4332
+ async change(args, cwd, run = this.write) {
4333
+ const { out, ok, fail } = await this.invoke([...args, "--json"], cwd, run);
4334
+ if (!ok || !out)
4335
+ throw fail(`kioku ${args.slice(0, 2).join(" ")} failed`);
4336
+ this.expire();
4337
+ return out;
4338
+ }
4339
+ // makes a brain on this machine only; kioku never makes a repository here
4340
+ async createBrain(input) {
4341
+ assertRepo(input.repo, "the brain");
4342
+ if (!TIERS.includes(input.tier))
4343
+ throw new KiokuError(`the tier is one of ${TIERS.join(", ")}`, 400);
4344
+ if (input.team)
4345
+ assertRepo(input.team, "the team brain");
4346
+ if (input.language && !LANGUAGE.test(input.language))
4347
+ throw new KiokuError("the language is a tag such as en or es", 400);
4348
+ const args = [
4349
+ "brain",
4350
+ "create",
4351
+ input.repo,
4352
+ "--tier",
4353
+ input.tier,
4354
+ ...input.team && input.tier === "project" ? ["--team", input.team] : [],
4355
+ ...input.language ? ["--language", input.language] : []
4356
+ ];
4357
+ const { brain, name, path } = await this.change(args, homedir5());
4358
+ return { brain, name, path };
4359
+ }
4360
+ async addArea(key, area, summary) {
4361
+ if (!AREA.test(area))
4362
+ throw new KiokuError("an area is letters, digits, dots, dashes and underscores, with / between parts", 400);
4363
+ if (!summary)
4364
+ throw new KiokuError("say what the area is for", 400);
4365
+ const brain = await this.findBrain(key);
4366
+ const out = await this.change(["namespace", "add", area, `--summary=${summary}`, "--brain", brain.key], brain.folder);
4367
+ return { brain: out.brain, namespace: out.namespace };
4368
+ }
4369
+ // writes the project's kioku.yaml; a brain the machine does not know is made first, since kioku links only known brains without a terminal
4370
+ async linkProject(input) {
4371
+ if (!input.brain && !input.team)
4372
+ throw new KiokuError("name a project brain, a team brain, or both", 400);
4373
+ const path = this.projects()[input.project]?.path;
4374
+ if (!path || !existsSync12(path))
4375
+ throw new KiokuError(`no CW project ${input.project} on this machine`, 404);
4376
+ if (input.brain)
4377
+ assertRepo(input.brain, "the project brain");
4378
+ if (input.team)
4379
+ assertRepo(input.team, "the team brain");
4380
+ const known = await this.listBrains();
4381
+ const isKnown = (repo) => known.some((b) => b.key === repo || b.key.endsWith(`/${repo}`));
4382
+ const created = [];
4383
+ if (input.team && !isKnown(input.team))
4384
+ created.push((await this.createBrain({ repo: input.team, tier: "team" })).brain);
4385
+ if (input.brain && !isKnown(input.brain))
4386
+ created.push((await this.createBrain({ repo: input.brain, tier: "project", team: input.team })).brain);
4387
+ const args = ["init", ...input.brain ? [input.brain] : [], ...input.team ? ["--team", input.team] : []];
4388
+ const { file } = await this.change(args, path);
4389
+ return { file, created };
4390
+ }
4391
+ // the only verb that reaches a git host: without `to`, kioku makes a private GitHub repository with gh
4392
+ async publish(key, to) {
4393
+ if (to?.startsWith("-"))
4394
+ throw new KiokuError("the address cannot start with -", 400);
4395
+ const brain = await this.findBrain(key);
4396
+ if (brain.remote)
4397
+ throw new KiokuError(`${brainName(brain)} already has a repository: ${brain.remote}`, 400);
4398
+ const out = await this.change(["brain", "publish", brain.key, ...to ? ["--to", to] : []], brain.folder, this.terminal ??= createTerminalRunner(envWithoutHost(), PUBLISH_TIMEOUT_MS));
4399
+ return { brain: out.brain, status: out.status, remote: out.remote ?? null };
4400
+ }
4203
4401
  // one viewer at a time; a second call reuses the running one
4204
4402
  async openViewer() {
4205
4403
  this.viewer ??= this.startViewer();
@@ -4239,7 +4437,7 @@ var init_kioku = __esm({
4239
4437
  const missing = err.code === "ENOENT";
4240
4438
  fail(new KiokuError(missing ? NOT_INSTALLED : err.message, missing ? 404 : 502));
4241
4439
  });
4242
- child.on("exit", (code) => {
4440
+ child.on("close", (code) => {
4243
4441
  if (this.viewer === started)
4244
4442
  this.viewer = null;
4245
4443
  fail(new KiokuError(tailOutput(stderr, 1) || `kioku web exited with code ${code}`));
@@ -4265,7 +4463,7 @@ var init_kioku = __esm({
4265
4463
 
4266
4464
  // ../core/dist/kioku-routes.js
4267
4465
  import { Hono as Hono4 } from "hono";
4268
- function kiokuRoutes(kioku = new Kioku()) {
4466
+ function kiokuRoutes(kioku = new Kioku(), options = {}) {
4269
4467
  const app = new Hono4();
4270
4468
  app.onError((err, c) => {
4271
4469
  if (err instanceof KiokuError)
@@ -4273,6 +4471,47 @@ function kiokuRoutes(kioku = new Kioku()) {
4273
4471
  throw err;
4274
4472
  });
4275
4473
  app.get("/status", async (c) => c.json(await kioku.status(c.req.query("fresh") === "1")));
4474
+ app.post("/pages", async (c) => {
4475
+ const input = await c.req.json().catch(() => ({}));
4476
+ const brain = input.brain?.trim();
4477
+ const title = input.title?.trim();
4478
+ const summary = input.summary?.trim();
4479
+ if (!brain || !input.type || !title || !summary || !input.body?.trim())
4480
+ return c.json({ error: "brain, type, title, summary and body are required" }, 400);
4481
+ const result2 = await kioku.save({ brain, type: input.type, title, summary, body: input.body, namespace: input.namespace?.trim() || void 0, create: input.create === true });
4482
+ return c.json(result2, result2.status === "done" ? 201 : 200);
4483
+ });
4484
+ app.post("/brains", async (c) => {
4485
+ const { repo, tier, team, language } = await body(c);
4486
+ return c.json(await kioku.createBrain({ repo: trim(repo) ?? "", tier: tier ?? "", team: trim(team), language: trim(language) }), 201);
4487
+ });
4488
+ app.post("/areas", async (c) => {
4489
+ const { brain, area, summary } = await body(c);
4490
+ return c.json(await kioku.addArea(brain ?? "", trim(area) ?? "", trim(summary) ?? ""), 201);
4491
+ });
4492
+ app.post("/link", async (c) => {
4493
+ const { project, brain, team } = await body(c);
4494
+ return c.json(await kioku.linkProject({ project: project ?? "", brain: trim(brain), team: trim(team) }));
4495
+ });
4496
+ app.post("/publish", async (c) => {
4497
+ const { brain, to, understood } = await body(c);
4498
+ if (understood !== true)
4499
+ return c.json({ error: "publishing needs the warning accepted" }, 400);
4500
+ return c.json(await kioku.publish(brain ?? "", trim(to)));
4501
+ });
4502
+ app.post("/remember", async (c) => {
4503
+ const { project, sessionDir, text } = await c.req.json().catch(() => ({}));
4504
+ const line = text?.trim();
4505
+ if (!project || !sessionDir || !line)
4506
+ return c.json({ error: "project, sessionDir and text are required" }, 400);
4507
+ if (line.length > REMEMBER_MAX)
4508
+ return c.json({ error: `keep it under ${REMEMBER_MAX} characters` }, 400);
4509
+ if (options.canRemember && !options.canRemember(project, sessionDir))
4510
+ return c.json({ error: "only Claude Code sessions have kioku's skill" }, 400);
4511
+ if (!options.type?.(project, sessionDir, rememberPrompt(line)))
4512
+ return c.json({ error: "the session has no live terminal" }, 409);
4513
+ return c.json({ ok: true });
4514
+ });
4276
4515
  app.post("/viewer", async (c) => c.json({ address: await kioku.openViewer() }));
4277
4516
  app.delete("/viewer", (c) => {
4278
4517
  kioku.stopViewer();
@@ -4280,10 +4519,14 @@ function kiokuRoutes(kioku = new Kioku()) {
4280
4519
  });
4281
4520
  return app;
4282
4521
  }
4522
+ var trim, REMEMBER_MAX, body;
4283
4523
  var init_kioku_routes = __esm({
4284
4524
  "../core/dist/kioku-routes.js"() {
4285
4525
  "use strict";
4286
4526
  init_kioku();
4527
+ trim = (value) => value?.trim() || void 0;
4528
+ REMEMBER_MAX = 500;
4529
+ body = (c) => c.req.json().catch(() => ({}));
4287
4530
  }
4288
4531
  });
4289
4532
 
@@ -4332,7 +4575,11 @@ function createForgeServer(options) {
4332
4575
  const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
4333
4576
  app.route("/api/liveframe", liveframeRoutes());
4334
4577
  const kioku = new Kioku({ projects: () => cwReader.getProjects() });
4335
- app.route("/api/kioku", kiokuRoutes(kioku));
4578
+ app.route("/api/kioku", kiokuRoutes(kioku, {
4579
+ type: (project, sessionDir, text) => ptyManager.typeLine(project, sessionDir, text),
4580
+ // kioku's plugin, and so its skill, exists only for Claude Code
4581
+ canRemember: (project, sessionDir) => cwReader.getSession(project, sessionDir)?.harness === "claude"
4582
+ }));
4336
4583
  app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
4337
4584
  app.post("/api/cw/terminal/kill", async (c) => {
4338
4585
  const { project, sessionDir } = await c.req.json();
@@ -4461,8 +4708,8 @@ data: ${JSON.stringify(data)}
4461
4708
  });
4462
4709
  app.put("/api/modules/:module/settings", async (c) => {
4463
4710
  const moduleId = c.req.param("module");
4464
- const body = await c.req.json();
4465
- for (const [key, value] of Object.entries(body)) {
4711
+ const body2 = await c.req.json();
4712
+ for (const [key, value] of Object.entries(body2)) {
4466
4713
  db.setModuleSetting(moduleId, key, String(value));
4467
4714
  }
4468
4715
  return c.json({ ok: true });
@@ -4730,8 +4977,8 @@ function waitForListening(server) {
4730
4977
  async function probePort(port, timeoutMs = 1500) {
4731
4978
  try {
4732
4979
  const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
4733
- const body = await res.json().catch(() => null);
4734
- return body?.status === "ok" && typeof body.modules === "number" ? "forge" : "other";
4980
+ const body2 = await res.json().catch(() => null);
4981
+ return body2?.status === "ok" && typeof body2.modules === "number" ? "forge" : "other";
4735
4982
  } catch (err) {
4736
4983
  return err.name === "TimeoutError" ? "unresponsive" : "other";
4737
4984
  }
@@ -4787,6 +5034,7 @@ __export(dist_exports, {
4787
5034
  portInUseMessage: () => portInUseMessage,
4788
5035
  probePort: () => probePort,
4789
5036
  quietSqliteWarning: () => quietSqliteWarning,
5037
+ rememberPrompt: () => rememberPrompt,
4790
5038
  resolveKiokuBin: () => resolveKiokuBin,
4791
5039
  resolveListenOptions: () => resolveListenOptions,
4792
5040
  skillRoutes: () => skillRoutes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-cw",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "A visual dashboard for your coding agents on Claude Code, Codex, Pi or OpenCode. Installs CW for you.",
5
5
  "license": "MIT",
6
6
  "type": "module",