forge-cw 0.1.9 → 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-DLM-Ix3c.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
  }
@@ -1311,10 +1311,10 @@ async function probeClaudeUsage(configDir, deps = {}) {
1311
1311
  });
1312
1312
  if (response.status === 401 || response.status === 403)
1313
1313
  return { state: "expired", windows: [], detail: "Claude rejected the stored token" };
1314
- const body = await response.text();
1314
+ const body2 = await response.text();
1315
1315
  if (!response.ok)
1316
1316
  return { state: "error", windows: [], detail: `Claude answered ${response.status}` };
1317
- return { state: "ok", windows: mapClaudeUsage(JSON.parse(body)), detail: null };
1317
+ return { state: "ok", windows: mapClaudeUsage(JSON.parse(body2)), detail: null };
1318
1318
  } catch (err) {
1319
1319
  return { state: "error", windows: [], detail: err instanceof Error ? err.message : String(err) };
1320
1320
  }
@@ -1539,9 +1539,9 @@ var init_login_manager = __esm({
1539
1539
  cwBin;
1540
1540
  spawn;
1541
1541
  entries = /* @__PURE__ */ new Map();
1542
- constructor(cwBin, spawn8 = spawnInPty) {
1542
+ constructor(cwBin, spawn9 = spawnInPty) {
1543
1543
  this.cwBin = cwBin;
1544
- this.spawn = spawn8;
1544
+ this.spawn = spawn9;
1545
1545
  }
1546
1546
  start(account, harness) {
1547
1547
  const key = `${account}::${harness}`;
@@ -1825,14 +1825,14 @@ function cwRoutes(reader, options = {}) {
1825
1825
  return c.json(reader.getAccounts());
1826
1826
  });
1827
1827
  app.post("/accounts", async (c) => {
1828
- const body = await c.req.json();
1829
- const harness = body.harness || void 0;
1830
- const provider = body.provider || void 0;
1831
- const model = body.model || void 0;
1832
- 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()) {
1833
1833
  return c.json({ ok: false, error: "Account name is required" }, 400);
1834
1834
  }
1835
- const trimmed = body.name.trim();
1835
+ const trimmed = body2.name.trim();
1836
1836
  if (!ACCOUNT_NAME_RE.test(trimmed)) {
1837
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);
1838
1838
  }
@@ -2896,11 +2896,11 @@ function skillRoutes(reader, options = {}) {
2896
2896
  });
2897
2897
  app.get("/marketplaces", (c) => c.json(listMarketplaces(reader)));
2898
2898
  app.post("/marketplaces", async (c) => {
2899
- const body = await c.req.json().catch(() => ({}));
2900
- const source = body.source?.trim() ?? "";
2899
+ const body2 = await c.req.json().catch(() => ({}));
2900
+ const source = body2.source?.trim() ?? "";
2901
2901
  if (!source || source.startsWith("-"))
2902
2902
  return c.json({ error: "A GitHub repo, URL or path is required" }, 400);
2903
- const account = knownAccount(body.account);
2903
+ const account = knownAccount(body2.account);
2904
2904
  if (!account)
2905
2905
  return c.json({ error: "Unknown account" }, 404);
2906
2906
  const failed = await runClaude("account", account, [["plugin", "marketplace", "add", source]], `Failed to add ${source}`);
@@ -2909,11 +2909,11 @@ function skillRoutes(reader, options = {}) {
2909
2909
  return c.json({ ok: true });
2910
2910
  });
2911
2911
  app.post("/plugins/install", async (c) => {
2912
- const body = await c.req.json().catch(() => ({}));
2913
- const id = body.id ?? "";
2912
+ const body2 = await c.req.json().catch(() => ({}));
2913
+ const id = body2.id ?? "";
2914
2914
  if (!PLUGIN_ID_RE.test(id))
2915
2915
  return c.json({ error: "Invalid plugin id" }, 400);
2916
- const account = knownAccount(body.account);
2916
+ const account = knownAccount(body2.account);
2917
2917
  if (!account)
2918
2918
  return c.json({ error: "Unknown account" }, 404);
2919
2919
  const { name, marketplace } = splitPluginId(id);
@@ -3259,7 +3259,7 @@ function createJevClassifier({ apiKey, model = "jev-latest", timeoutMs = 3e3, fe
3259
3259
  async classify(input) {
3260
3260
  if (input.exitCode !== null)
3261
3261
  return { state: "exited", confidence: 1, source: "jev" };
3262
- const body = {
3262
+ const body2 = {
3263
3263
  model,
3264
3264
  state: { harness: input.harness, seconds_since_last_output: Math.round(input.quietMs / 1e3), screen: jevScreen(input.screen ?? input.text) },
3265
3265
  questions: {
@@ -3273,7 +3273,7 @@ function createJevClassifier({ apiKey, model = "jev-latest", timeoutMs = 3e3, fe
3273
3273
  const res = await fetch2(JEV_URL, {
3274
3274
  method: "POST",
3275
3275
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3276
- body: JSON.stringify(body),
3276
+ body: JSON.stringify(body2),
3277
3277
  signal: AbortSignal.timeout(timeoutMs)
3278
3278
  });
3279
3279
  if (!res.ok)
@@ -4092,6 +4092,11 @@ import { existsSync as existsSync12 } from "node:fs";
4092
4092
  import { homedir as homedir5 } from "node:os";
4093
4093
  import { join as join13 } from "node:path";
4094
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
+ }
4095
4100
  function resolveKiokuBin(pathVar = process.env.PATH, home = homedir5()) {
4096
4101
  const fallback = join13(home, ".local", "bin", "kioku");
4097
4102
  return findOnPath("kioku", pathVar) ?? (existsSync12(fallback) ? fallback : "kioku");
@@ -4099,6 +4104,26 @@ function resolveKiokuBin(pathVar = process.env.PATH, home = homedir5()) {
4099
4104
  function rememberPrompt(text) {
4100
4105
  return `Remember this with kioku: ${text.replace(/\s+/g, " ").trim()}`;
4101
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
+ }
4102
4127
  function parseJson(text) {
4103
4128
  try {
4104
4129
  return JSON.parse(text);
@@ -4118,7 +4143,7 @@ async function mapLimit(items, limit, fn) {
4118
4143
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
4119
4144
  return out;
4120
4145
  }
4121
- var PAGE_TYPES, KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, WRITE_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;
4122
4147
  var init_kioku = __esm({
4123
4148
  "../core/dist/kioku.js"() {
4124
4149
  "use strict";
@@ -4144,15 +4169,21 @@ var init_kioku = __esm({
4144
4169
  NOT_INSTALLED = "kioku is not installed on this machine";
4145
4170
  START_TIMEOUT_MS = 2e4;
4146
4171
  WRITE_TIMEOUT_MS = 6e4;
4172
+ PUBLISH_TIMEOUT_MS = 3 * 6e4;
4147
4173
  CACHE_MS2 = 5 * 6e4;
4148
4174
  ACTIVITY_SINCE = "2w";
4149
4175
  ACTIVITY_LIMIT = 40;
4150
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]+)*$/;
4151
4181
  MISSING = { installed: false, version: null, brains: [], projects: {}, activity: [] };
4152
4182
  brainName = (b) => b.name ?? b.key;
4153
4183
  Kioku = class {
4154
4184
  run;
4155
4185
  write;
4186
+ terminal;
4156
4187
  startTimeoutMs;
4157
4188
  projects;
4158
4189
  now;
@@ -4163,6 +4194,7 @@ var init_kioku = __esm({
4163
4194
  this.binPath = options.bin;
4164
4195
  this.run = options.run ?? createRunner();
4165
4196
  this.write = options.write ?? createRunner(envWithoutHost(), WRITE_TIMEOUT_MS);
4197
+ this.terminal = options.terminal;
4166
4198
  this.startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
4167
4199
  this.projects = options.projects ?? (() => ({}));
4168
4200
  this.now = options.now ?? Date.now;
@@ -4244,10 +4276,7 @@ var init_kioku = __esm({
4244
4276
  const type = PAGE_TYPES[input.type];
4245
4277
  if (!type)
4246
4278
  throw new KiokuError(`unknown page type ${input.type}`, 400);
4247
- const brains = this.snapshot ? (await this.snapshot.value).brains : (await this.json(["brain", "list"], homedir5()))?.brains ?? [];
4248
- const brain = brains.find((b) => b.key === input.brain);
4249
- if (!brain?.onDisk)
4250
- throw new KiokuError(`no brain ${input.brain} on this machine`, 404);
4279
+ const brain = await this.findBrain(input.brain);
4251
4280
  const args = [
4252
4281
  "page",
4253
4282
  "save",
@@ -4266,19 +4295,108 @@ var init_kioku = __esm({
4266
4295
  ...input.create ? ["--create"] : [],
4267
4296
  "--json"
4268
4297
  ];
4269
- const result2 = await this.write(this.bin, args, brain.folder, input.body);
4270
- if (result2.code === 127)
4271
- throw new KiokuError(NOT_INSTALLED, 404);
4272
- const out = parseJson(result2.stdout);
4298
+ const { out, fail } = await this.invoke(args, brain.folder, this.write, input.body);
4273
4299
  if (out?.status === "done" && out.path) {
4274
- this.snapshot = null;
4300
+ this.expire();
4275
4301
  return { status: "done", brain: brainName(brain), path: out.path, publication: out.publication ?? null };
4276
4302
  }
4277
4303
  if (out?.status === "similar")
4278
4304
  return { status: "similar", similar: (out.similar ?? []).map(({ brain: brain2, path, title, summary }) => ({ brain: brain2, path, title, summary })) };
4279
4305
  if (out?.status === "namespace")
4280
4306
  return { status: "namespace", namespaces: (out.namespaces ?? []).map(({ namespace, summary }) => ({ namespace, summary })) };
4281
- throw new KiokuError(out?.error?.message ?? (tailOutput(result2.stderr, 1) || "kioku could not save the page"), 400);
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 };
4282
4400
  }
4283
4401
  // one viewer at a time; a second call reuses the running one
4284
4402
  async openViewer() {
@@ -4363,6 +4481,24 @@ function kiokuRoutes(kioku = new Kioku(), options = {}) {
4363
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 });
4364
4482
  return c.json(result2, result2.status === "done" ? 201 : 200);
4365
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
+ });
4366
4502
  app.post("/remember", async (c) => {
4367
4503
  const { project, sessionDir, text } = await c.req.json().catch(() => ({}));
4368
4504
  const line = text?.trim();
@@ -4383,12 +4519,14 @@ function kiokuRoutes(kioku = new Kioku(), options = {}) {
4383
4519
  });
4384
4520
  return app;
4385
4521
  }
4386
- var REMEMBER_MAX;
4522
+ var trim, REMEMBER_MAX, body;
4387
4523
  var init_kioku_routes = __esm({
4388
4524
  "../core/dist/kioku-routes.js"() {
4389
4525
  "use strict";
4390
4526
  init_kioku();
4527
+ trim = (value) => value?.trim() || void 0;
4391
4528
  REMEMBER_MAX = 500;
4529
+ body = (c) => c.req.json().catch(() => ({}));
4392
4530
  }
4393
4531
  });
4394
4532
 
@@ -4570,8 +4708,8 @@ data: ${JSON.stringify(data)}
4570
4708
  });
4571
4709
  app.put("/api/modules/:module/settings", async (c) => {
4572
4710
  const moduleId = c.req.param("module");
4573
- const body = await c.req.json();
4574
- for (const [key, value] of Object.entries(body)) {
4711
+ const body2 = await c.req.json();
4712
+ for (const [key, value] of Object.entries(body2)) {
4575
4713
  db.setModuleSetting(moduleId, key, String(value));
4576
4714
  }
4577
4715
  return c.json({ ok: true });
@@ -4839,8 +4977,8 @@ function waitForListening(server) {
4839
4977
  async function probePort(port, timeoutMs = 1500) {
4840
4978
  try {
4841
4979
  const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
4842
- const body = await res.json().catch(() => null);
4843
- 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";
4844
4982
  } catch (err) {
4845
4983
  return err.name === "TimeoutError" ? "unresponsive" : "other";
4846
4984
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-cw",
3
- "version": "0.1.9",
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",