forge-cw 0.1.8 → 0.1.9

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-DLM-Ix3c.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
@@ -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) {
@@ -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);
@@ -4074,6 +4096,9 @@ function resolveKiokuBin(pathVar = process.env.PATH, home = homedir5()) {
4074
4096
  const fallback = join13(home, ".local", "bin", "kioku");
4075
4097
  return findOnPath("kioku", pathVar) ?? (existsSync12(fallback) ? fallback : "kioku");
4076
4098
  }
4099
+ function rememberPrompt(text) {
4100
+ return `Remember this with kioku: ${text.replace(/\s+/g, " ").trim()}`;
4101
+ }
4077
4102
  function parseJson(text) {
4078
4103
  try {
4079
4104
  return JSON.parse(text);
@@ -4093,13 +4118,22 @@ async function mapLimit(items, limit, fn) {
4093
4118
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
4094
4119
  return out;
4095
4120
  }
4096
- var KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, CACHE_MS2, ACTIVITY_SINCE, ACTIVITY_LIMIT, PARALLEL, MISSING, brainName, Kioku;
4121
+ var PAGE_TYPES, KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, WRITE_TIMEOUT_MS, CACHE_MS2, ACTIVITY_SINCE, ACTIVITY_LIMIT, PARALLEL, MISSING, brainName, Kioku;
4097
4122
  var init_kioku = __esm({
4098
4123
  "../core/dist/kioku.js"() {
4099
4124
  "use strict";
4100
4125
  init_task_review();
4126
+ init_cw_doctor();
4101
4127
  init_cw_install();
4102
4128
  init_output();
4129
+ PAGE_TYPES = {
4130
+ memory: { kind: "memory" },
4131
+ instinct: { kind: "memory", sub: "instinct" },
4132
+ decision: { kind: "knowledge", sub: "decision" },
4133
+ playbook: { kind: "knowledge", sub: "playbook" },
4134
+ context: { kind: "knowledge", sub: "context" },
4135
+ rule: { kind: "rule" }
4136
+ };
4103
4137
  KiokuError = class extends Error {
4104
4138
  status;
4105
4139
  constructor(message, status = 502) {
@@ -4109,6 +4143,7 @@ var init_kioku = __esm({
4109
4143
  };
4110
4144
  NOT_INSTALLED = "kioku is not installed on this machine";
4111
4145
  START_TIMEOUT_MS = 2e4;
4146
+ WRITE_TIMEOUT_MS = 6e4;
4112
4147
  CACHE_MS2 = 5 * 6e4;
4113
4148
  ACTIVITY_SINCE = "2w";
4114
4149
  ACTIVITY_LIMIT = 40;
@@ -4117,6 +4152,7 @@ var init_kioku = __esm({
4117
4152
  brainName = (b) => b.name ?? b.key;
4118
4153
  Kioku = class {
4119
4154
  run;
4155
+ write;
4120
4156
  startTimeoutMs;
4121
4157
  projects;
4122
4158
  now;
@@ -4126,6 +4162,7 @@ var init_kioku = __esm({
4126
4162
  constructor(options = {}) {
4127
4163
  this.binPath = options.bin;
4128
4164
  this.run = options.run ?? createRunner();
4165
+ this.write = options.write ?? createRunner(envWithoutHost(), WRITE_TIMEOUT_MS);
4129
4166
  this.startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
4130
4167
  this.projects = options.projects ?? (() => ({}));
4131
4168
  this.now = options.now ?? Date.now;
@@ -4163,17 +4200,18 @@ var init_kioku = __esm({
4163
4200
  const entries = Object.entries(this.projects()).filter(([, p]) => existsSync12(p.path));
4164
4201
  const readBrain = async (b) => {
4165
4202
  if (!b.onDisk)
4166
- return { kinds: {}, writes: [] };
4203
+ return { kinds: {}, namespaces: [], writes: [] };
4167
4204
  const [ns, log] = await Promise.all([
4168
4205
  this.json(["namespace", "list", "--brain", b.key], b.folder),
4169
4206
  this.json(["brain", "log", "--since", ACTIVITY_SINCE, "--brain", b.key], b.folder)
4170
4207
  ]);
4171
4208
  const kinds = {};
4172
- for (const n of ns?.brains?.find((x) => x.brain === brainName(b))?.namespaces ?? []) {
4209
+ const namespaces = ns?.brains?.find((x) => x.brain === brainName(b))?.namespaces ?? [];
4210
+ for (const n of namespaces) {
4173
4211
  for (const [kind, count] of Object.entries(n.pages))
4174
4212
  kinds[kind] = (kinds[kind] ?? 0) + count;
4175
4213
  }
4176
- return { kinds, writes: log?.writes ?? [] };
4214
+ return { kinds, namespaces: namespaces.map((n) => n.namespace), writes: log?.writes ?? [] };
4177
4215
  };
4178
4216
  const readChain = async (path) => (await this.json(["brain", "list"], path))?.chain ?? [];
4179
4217
  const [perBrain, chains] = await Promise.all([
@@ -4182,12 +4220,13 @@ var init_kioku = __esm({
4182
4220
  ]);
4183
4221
  const activity = perBrain.flatMap((p) => p.writes).sort((x, y) => y.date.localeCompare(x.date));
4184
4222
  const brains = listed.map((b, i) => {
4185
- const { kinds } = perBrain[i];
4223
+ const { kinds, namespaces } = perBrain[i];
4186
4224
  const name = brainName(b);
4187
4225
  return {
4188
4226
  ...b,
4189
4227
  pages: Object.values(kinds).reduce((sum, n) => sum + n, 0),
4190
4228
  kinds,
4229
+ namespaces,
4191
4230
  touched: activity.find((w) => w.brain === name)?.date ?? null
4192
4231
  };
4193
4232
  });
@@ -4200,6 +4239,47 @@ var init_kioku = __esm({
4200
4239
  activity: activity.slice(0, ACTIVITY_LIMIT).map(({ verb, title, path, date, author, brain }) => ({ verb, title, path, date, author, brain }))
4201
4240
  };
4202
4241
  }
4242
+ // writes through kioku, which validates, commits and pushes a shared brain; Forge never writes a brain's files
4243
+ async save(input) {
4244
+ const type = PAGE_TYPES[input.type];
4245
+ if (!type)
4246
+ 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);
4251
+ const args = [
4252
+ "page",
4253
+ "save",
4254
+ "--brain",
4255
+ brain.key,
4256
+ "--kind",
4257
+ type.kind,
4258
+ ...type.sub ? ["--sub", type.sub] : [],
4259
+ "--title",
4260
+ input.title,
4261
+ "--summary",
4262
+ input.summary,
4263
+ "--source",
4264
+ "Forge",
4265
+ ...input.namespace ? ["--namespace", input.namespace] : [],
4266
+ ...input.create ? ["--create"] : [],
4267
+ "--json"
4268
+ ];
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);
4273
+ if (out?.status === "done" && out.path) {
4274
+ this.snapshot = null;
4275
+ return { status: "done", brain: brainName(brain), path: out.path, publication: out.publication ?? null };
4276
+ }
4277
+ if (out?.status === "similar")
4278
+ return { status: "similar", similar: (out.similar ?? []).map(({ brain: brain2, path, title, summary }) => ({ brain: brain2, path, title, summary })) };
4279
+ if (out?.status === "namespace")
4280
+ 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);
4282
+ }
4203
4283
  // one viewer at a time; a second call reuses the running one
4204
4284
  async openViewer() {
4205
4285
  this.viewer ??= this.startViewer();
@@ -4239,7 +4319,7 @@ var init_kioku = __esm({
4239
4319
  const missing = err.code === "ENOENT";
4240
4320
  fail(new KiokuError(missing ? NOT_INSTALLED : err.message, missing ? 404 : 502));
4241
4321
  });
4242
- child.on("exit", (code) => {
4322
+ child.on("close", (code) => {
4243
4323
  if (this.viewer === started)
4244
4324
  this.viewer = null;
4245
4325
  fail(new KiokuError(tailOutput(stderr, 1) || `kioku web exited with code ${code}`));
@@ -4265,7 +4345,7 @@ var init_kioku = __esm({
4265
4345
 
4266
4346
  // ../core/dist/kioku-routes.js
4267
4347
  import { Hono as Hono4 } from "hono";
4268
- function kiokuRoutes(kioku = new Kioku()) {
4348
+ function kiokuRoutes(kioku = new Kioku(), options = {}) {
4269
4349
  const app = new Hono4();
4270
4350
  app.onError((err, c) => {
4271
4351
  if (err instanceof KiokuError)
@@ -4273,6 +4353,29 @@ function kiokuRoutes(kioku = new Kioku()) {
4273
4353
  throw err;
4274
4354
  });
4275
4355
  app.get("/status", async (c) => c.json(await kioku.status(c.req.query("fresh") === "1")));
4356
+ app.post("/pages", async (c) => {
4357
+ const input = await c.req.json().catch(() => ({}));
4358
+ const brain = input.brain?.trim();
4359
+ const title = input.title?.trim();
4360
+ const summary = input.summary?.trim();
4361
+ if (!brain || !input.type || !title || !summary || !input.body?.trim())
4362
+ return c.json({ error: "brain, type, title, summary and body are required" }, 400);
4363
+ 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
+ return c.json(result2, result2.status === "done" ? 201 : 200);
4365
+ });
4366
+ app.post("/remember", async (c) => {
4367
+ const { project, sessionDir, text } = await c.req.json().catch(() => ({}));
4368
+ const line = text?.trim();
4369
+ if (!project || !sessionDir || !line)
4370
+ return c.json({ error: "project, sessionDir and text are required" }, 400);
4371
+ if (line.length > REMEMBER_MAX)
4372
+ return c.json({ error: `keep it under ${REMEMBER_MAX} characters` }, 400);
4373
+ if (options.canRemember && !options.canRemember(project, sessionDir))
4374
+ return c.json({ error: "only Claude Code sessions have kioku's skill" }, 400);
4375
+ if (!options.type?.(project, sessionDir, rememberPrompt(line)))
4376
+ return c.json({ error: "the session has no live terminal" }, 409);
4377
+ return c.json({ ok: true });
4378
+ });
4276
4379
  app.post("/viewer", async (c) => c.json({ address: await kioku.openViewer() }));
4277
4380
  app.delete("/viewer", (c) => {
4278
4381
  kioku.stopViewer();
@@ -4280,10 +4383,12 @@ function kiokuRoutes(kioku = new Kioku()) {
4280
4383
  });
4281
4384
  return app;
4282
4385
  }
4386
+ var REMEMBER_MAX;
4283
4387
  var init_kioku_routes = __esm({
4284
4388
  "../core/dist/kioku-routes.js"() {
4285
4389
  "use strict";
4286
4390
  init_kioku();
4391
+ REMEMBER_MAX = 500;
4287
4392
  }
4288
4393
  });
4289
4394
 
@@ -4332,7 +4437,11 @@ function createForgeServer(options) {
4332
4437
  const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
4333
4438
  app.route("/api/liveframe", liveframeRoutes());
4334
4439
  const kioku = new Kioku({ projects: () => cwReader.getProjects() });
4335
- app.route("/api/kioku", kiokuRoutes(kioku));
4440
+ app.route("/api/kioku", kiokuRoutes(kioku, {
4441
+ type: (project, sessionDir, text) => ptyManager.typeLine(project, sessionDir, text),
4442
+ // kioku's plugin, and so its skill, exists only for Claude Code
4443
+ canRemember: (project, sessionDir) => cwReader.getSession(project, sessionDir)?.harness === "claude"
4444
+ }));
4336
4445
  app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
4337
4446
  app.post("/api/cw/terminal/kill", async (c) => {
4338
4447
  const { project, sessionDir } = await c.req.json();
@@ -4787,6 +4896,7 @@ __export(dist_exports, {
4787
4896
  portInUseMessage: () => portInUseMessage,
4788
4897
  probePort: () => probePort,
4789
4898
  quietSqliteWarning: () => quietSqliteWarning,
4899
+ rememberPrompt: () => rememberPrompt,
4790
4900
  resolveKiokuBin: () => resolveKiokuBin,
4791
4901
  resolveListenOptions: () => resolveListenOptions,
4792
4902
  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.9",
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",