forge-cw 0.1.7 → 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.
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) {
@@ -1526,9 +1539,9 @@ var init_login_manager = __esm({
1526
1539
  cwBin;
1527
1540
  spawn;
1528
1541
  entries = /* @__PURE__ */ new Map();
1529
- constructor(cwBin, spawn7 = spawnInPty) {
1542
+ constructor(cwBin, spawn8 = spawnInPty) {
1530
1543
  this.cwBin = cwBin;
1531
- this.spawn = spawn7;
1544
+ this.spawn = spawn8;
1532
1545
  }
1533
1546
  start(account, harness) {
1534
1547
  const key = `${account}::${harness}`;
@@ -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);
@@ -3932,20 +3954,458 @@ var init_liveframe_routes = __esm({
3932
3954
  }
3933
3955
  });
3934
3956
 
3935
- // ../core/dist/server.js
3957
+ // ../core/dist/cw-install.js
3958
+ import { execFile as execFile5 } from "node:child_process";
3959
+ import { createHash as createHash2 } from "node:crypto";
3960
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
3961
+ import { delimiter as delimiter2, join as join12 } from "node:path";
3962
+ function readCwVersion(file) {
3963
+ try {
3964
+ const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync9(file, "utf-8"));
3965
+ return match ? match[1] : null;
3966
+ } catch {
3967
+ return null;
3968
+ }
3969
+ }
3970
+ function compareVersions(a, b) {
3971
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
3972
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
3973
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
3974
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
3975
+ if (diff !== 0)
3976
+ return diff;
3977
+ }
3978
+ return 0;
3979
+ }
3980
+ function readCwBundle(dir) {
3981
+ try {
3982
+ const { ref } = JSON.parse(readFileSync9(join12(dir, ".bundle.json"), "utf-8"));
3983
+ const version = readCwVersion(join12(dir, "cw"));
3984
+ if (!ref || !version || !existsSync11(join12(dir, "install.sh")))
3985
+ return null;
3986
+ return { dir, version, ref };
3987
+ } catch {
3988
+ return null;
3989
+ }
3990
+ }
3991
+ function decideCwInstall({ bundle, installed, marker, cwOnPath, skip }) {
3992
+ if (skip)
3993
+ return { action: "skip", reason: "FORGE_SKIP_CW_INSTALL is set" };
3994
+ if (!bundle)
3995
+ return { action: "skip", reason: "this Forge carries no CW" };
3996
+ if (!installed) {
3997
+ return cwOnPath ? { action: "skip", reason: "using the cw found on PATH" } : { action: "install", reason: "CW is not installed" };
3998
+ }
3999
+ if (!installed.version)
4000
+ return { action: "skip", reason: "cannot read the installed CW version" };
4001
+ const newer = compareVersions(bundle.version, installed.version);
4002
+ const ownedByForge = marker !== null && marker.sha256 === installed.sha256;
4003
+ if (ownedByForge) {
4004
+ if (marker.ref === bundle.ref)
4005
+ return { action: "skip", reason: `CW ${installed.version} is current` };
4006
+ return newer >= 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `installed CW ${installed.version} is newer than ${bundle.version}` };
4007
+ }
4008
+ return newer > 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `CW ${installed.version} is current` };
4009
+ }
4010
+ function findOnPath(name, pathVar, excludeDir) {
4011
+ for (const dir of (pathVar ?? "").split(delimiter2)) {
4012
+ if (!dir || dir === excludeDir)
4013
+ continue;
4014
+ const candidate = join12(dir, name);
4015
+ try {
4016
+ if (!statSync3(candidate).isFile())
4017
+ continue;
4018
+ accessSync2(candidate, constants2.X_OK);
4019
+ return candidate;
4020
+ } catch {
4021
+ }
4022
+ }
4023
+ return null;
4024
+ }
4025
+ function pathWithCw(cwHome, pathVar) {
4026
+ const bin = join12(cwHome, "bin");
4027
+ const rest = (pathVar ?? "").split(delimiter2).filter((dir) => dir && dir !== bin);
4028
+ return [bin, ...rest].join(delimiter2);
4029
+ }
4030
+ function readMarker(cwHome) {
4031
+ try {
4032
+ return JSON.parse(readFileSync9(join12(cwHome, MARKER_FILE), "utf-8"));
4033
+ } catch {
4034
+ return null;
4035
+ }
4036
+ }
4037
+ function failureMessage2(output) {
4038
+ const lines2 = output.replace(ANSI_RE3, "").split("\n").map((l) => l.trim()).filter(Boolean);
4039
+ const errors = lines2.filter((l) => l.startsWith("[\u2717]")).map((l) => l.slice(3).trim());
4040
+ return (errors.length > 0 ? errors : lines2.slice(-3)).join(" ") || "the CW installer failed";
4041
+ }
4042
+ async function ensureCw(options) {
4043
+ const { cwHome, bundleDir } = options;
4044
+ const env = options.env ?? process.env;
4045
+ const run = options.run ?? runInstall;
4046
+ const bin = join12(cwHome, "bin", "cw");
4047
+ const bundle = readCwBundle(bundleDir);
4048
+ const installed = existsSync11(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
4049
+ const decision = decideCwInstall({
4050
+ bundle,
4051
+ installed,
4052
+ marker: readMarker(cwHome),
4053
+ cwOnPath: findOnPath("cw", env.PATH, join12(cwHome, "bin")) !== null,
4054
+ skip: env.FORGE_SKIP_CW_INSTALL === "1"
4055
+ });
4056
+ if (decision.action === "skip" || !bundle)
4057
+ return { action: "skipped", message: decision.reason };
4058
+ const { code, output } = await run(join12(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
4059
+ if (code !== 0 || !existsSync11(bin))
4060
+ return { action: "failed", message: failureMessage2(output) };
4061
+ const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
4062
+ writeFileSync4(join12(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
4063
+ return {
4064
+ action: decision.action === "install" ? "installed" : "updated",
4065
+ version: bundle.version,
4066
+ message: decision.reason
4067
+ };
4068
+ }
4069
+ var MARKER_FILE, INSTALL_TIMEOUT_MS2, ANSI_RE3, sha256Of, runInstall;
4070
+ var init_cw_install = __esm({
4071
+ "../core/dist/cw-install.js"() {
4072
+ "use strict";
4073
+ MARKER_FILE = ".forge-cw.json";
4074
+ INSTALL_TIMEOUT_MS2 = 12e4;
4075
+ ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
4076
+ sha256Of = (file) => createHash2("sha256").update(readFileSync9(file)).digest("hex");
4077
+ runInstall = (script, args, env) => new Promise((resolve5) => {
4078
+ execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
4079
+ const output = `${String(stdout)}${String(stderr)}`;
4080
+ if (!err)
4081
+ return resolve5({ code: 0, output });
4082
+ const code = err.code;
4083
+ resolve5({ code: typeof code === "number" ? code : 1, output: output || err.message });
4084
+ });
4085
+ });
4086
+ }
4087
+ });
4088
+
4089
+ // ../core/dist/kioku.js
4090
+ import { spawn as spawn7 } from "node:child_process";
4091
+ import { existsSync as existsSync12 } from "node:fs";
4092
+ import { homedir as homedir5 } from "node:os";
4093
+ import { join as join13 } from "node:path";
4094
+ import { createInterface } from "node:readline";
4095
+ function resolveKiokuBin(pathVar = process.env.PATH, home = homedir5()) {
4096
+ const fallback = join13(home, ".local", "bin", "kioku");
4097
+ return findOnPath("kioku", pathVar) ?? (existsSync12(fallback) ? fallback : "kioku");
4098
+ }
4099
+ function rememberPrompt(text) {
4100
+ return `Remember this with kioku: ${text.replace(/\s+/g, " ").trim()}`;
4101
+ }
4102
+ function parseJson(text) {
4103
+ try {
4104
+ return JSON.parse(text);
4105
+ } catch {
4106
+ return null;
4107
+ }
4108
+ }
4109
+ async function mapLimit(items, limit, fn) {
4110
+ const out = new Array(items.length);
4111
+ let next = 0;
4112
+ const worker = async () => {
4113
+ while (next < items.length) {
4114
+ const i = next++;
4115
+ out[i] = await fn(items[i]);
4116
+ }
4117
+ };
4118
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
4119
+ return out;
4120
+ }
4121
+ var PAGE_TYPES, KiokuError, NOT_INSTALLED, START_TIMEOUT_MS, WRITE_TIMEOUT_MS, CACHE_MS2, ACTIVITY_SINCE, ACTIVITY_LIMIT, PARALLEL, MISSING, brainName, Kioku;
4122
+ var init_kioku = __esm({
4123
+ "../core/dist/kioku.js"() {
4124
+ "use strict";
4125
+ init_task_review();
4126
+ init_cw_doctor();
4127
+ init_cw_install();
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
+ };
4137
+ KiokuError = class extends Error {
4138
+ status;
4139
+ constructor(message, status = 502) {
4140
+ super(message);
4141
+ this.status = status;
4142
+ }
4143
+ };
4144
+ NOT_INSTALLED = "kioku is not installed on this machine";
4145
+ START_TIMEOUT_MS = 2e4;
4146
+ WRITE_TIMEOUT_MS = 6e4;
4147
+ CACHE_MS2 = 5 * 6e4;
4148
+ ACTIVITY_SINCE = "2w";
4149
+ ACTIVITY_LIMIT = 40;
4150
+ PARALLEL = 6;
4151
+ MISSING = { installed: false, version: null, brains: [], projects: {}, activity: [] };
4152
+ brainName = (b) => b.name ?? b.key;
4153
+ Kioku = class {
4154
+ run;
4155
+ write;
4156
+ startTimeoutMs;
4157
+ projects;
4158
+ now;
4159
+ binPath;
4160
+ viewer = null;
4161
+ snapshot = null;
4162
+ constructor(options = {}) {
4163
+ this.binPath = options.bin;
4164
+ this.run = options.run ?? createRunner();
4165
+ this.write = options.write ?? createRunner(envWithoutHost(), WRITE_TIMEOUT_MS);
4166
+ this.startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
4167
+ this.projects = options.projects ?? (() => ({}));
4168
+ this.now = options.now ?? Date.now;
4169
+ }
4170
+ // resolved on first use, so a Forge that never opens Memory does not search PATH
4171
+ get bin() {
4172
+ return this.binPath ??= resolveKiokuBin();
4173
+ }
4174
+ async status(fresh = false) {
4175
+ const previous = this.snapshot;
4176
+ if (fresh || !previous || this.now() - previous.at > CACHE_MS2) {
4177
+ const value = this.read();
4178
+ this.snapshot = { at: this.now(), value };
4179
+ value.catch(() => {
4180
+ if (this.snapshot?.value === value)
4181
+ this.snapshot = previous;
4182
+ });
4183
+ }
4184
+ const pending = fresh || !previous ? this.snapshot.value : previous.value;
4185
+ const [snapshot, viewer] = await Promise.all([pending, this.viewer?.catch(() => null) ?? null]);
4186
+ return { ...snapshot, viewer: viewer?.address ?? null };
4187
+ }
4188
+ async json(args, cwd) {
4189
+ const result2 = await this.run(this.bin, [...args, "--json"], cwd);
4190
+ return result2.code === 0 ? parseJson(result2.stdout) : null;
4191
+ }
4192
+ async read() {
4193
+ const [version, list] = await Promise.all([
4194
+ this.run(this.bin, ["--version"], homedir5()),
4195
+ this.json(["brain", "list"], homedir5())
4196
+ ]);
4197
+ if (version.code !== 0)
4198
+ return MISSING;
4199
+ const listed = list?.brains ?? [];
4200
+ const entries = Object.entries(this.projects()).filter(([, p]) => existsSync12(p.path));
4201
+ const readBrain = async (b) => {
4202
+ if (!b.onDisk)
4203
+ return { kinds: {}, namespaces: [], writes: [] };
4204
+ const [ns, log] = await Promise.all([
4205
+ this.json(["namespace", "list", "--brain", b.key], b.folder),
4206
+ this.json(["brain", "log", "--since", ACTIVITY_SINCE, "--brain", b.key], b.folder)
4207
+ ]);
4208
+ const kinds = {};
4209
+ const namespaces = ns?.brains?.find((x) => x.brain === brainName(b))?.namespaces ?? [];
4210
+ for (const n of namespaces) {
4211
+ for (const [kind, count] of Object.entries(n.pages))
4212
+ kinds[kind] = (kinds[kind] ?? 0) + count;
4213
+ }
4214
+ return { kinds, namespaces: namespaces.map((n) => n.namespace), writes: log?.writes ?? [] };
4215
+ };
4216
+ const readChain = async (path) => (await this.json(["brain", "list"], path))?.chain ?? [];
4217
+ const [perBrain, chains] = await Promise.all([
4218
+ mapLimit(listed, PARALLEL / 2, readBrain),
4219
+ mapLimit(entries, PARALLEL / 2, ([, p]) => readChain(p.path))
4220
+ ]);
4221
+ const activity = perBrain.flatMap((p) => p.writes).sort((x, y) => y.date.localeCompare(x.date));
4222
+ const brains = listed.map((b, i) => {
4223
+ const { kinds, namespaces } = perBrain[i];
4224
+ const name = brainName(b);
4225
+ return {
4226
+ ...b,
4227
+ pages: Object.values(kinds).reduce((sum, n) => sum + n, 0),
4228
+ kinds,
4229
+ namespaces,
4230
+ touched: activity.find((w) => w.brain === name)?.date ?? null
4231
+ };
4232
+ });
4233
+ const projects = Object.fromEntries(entries.map(([name], i) => [name, chains[i].map(({ name: name2, tier }) => ({ name: name2, tier }))]));
4234
+ return {
4235
+ installed: true,
4236
+ version: parseJson(version.stdout)?.version ?? null,
4237
+ brains,
4238
+ projects,
4239
+ activity: activity.slice(0, ACTIVITY_LIMIT).map(({ verb, title, path, date, author, brain }) => ({ verb, title, path, date, author, brain }))
4240
+ };
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
+ }
4283
+ // one viewer at a time; a second call reuses the running one
4284
+ async openViewer() {
4285
+ this.viewer ??= this.startViewer();
4286
+ return (await this.viewer).address;
4287
+ }
4288
+ startViewer() {
4289
+ const started = new Promise((resolve5, reject) => {
4290
+ const child = spawn7(this.bin, ["web", "--json", "--no-open"], { cwd: homedir5(), stdio: ["ignore", "pipe", "pipe"] });
4291
+ const lines2 = createInterface({ input: child.stdout });
4292
+ let stderr = "";
4293
+ let settled = false;
4294
+ const finish = () => {
4295
+ settled = true;
4296
+ clearTimeout(timer);
4297
+ lines2.close();
4298
+ child.stdout?.resume();
4299
+ };
4300
+ const fail = (err) => {
4301
+ if (settled)
4302
+ return;
4303
+ finish();
4304
+ child.kill();
4305
+ reject(err);
4306
+ };
4307
+ const timer = setTimeout(() => fail(new KiokuError("kioku web did not start in time")), this.startTimeoutMs);
4308
+ child.stderr?.on("data", (chunk) => {
4309
+ stderr = (stderr + chunk.toString()).slice(-4096);
4310
+ });
4311
+ lines2.on("line", (line) => {
4312
+ const event = parseJson(line);
4313
+ if (settled || event?.event !== "listening" || !event.address?.startsWith("http://127.0.0.1:"))
4314
+ return;
4315
+ finish();
4316
+ resolve5({ child, address: event.address });
4317
+ });
4318
+ child.on("error", (err) => {
4319
+ const missing = err.code === "ENOENT";
4320
+ fail(new KiokuError(missing ? NOT_INSTALLED : err.message, missing ? 404 : 502));
4321
+ });
4322
+ child.on("close", (code) => {
4323
+ if (this.viewer === started)
4324
+ this.viewer = null;
4325
+ fail(new KiokuError(tailOutput(stderr, 1) || `kioku web exited with code ${code}`));
4326
+ });
4327
+ });
4328
+ started.catch(() => {
4329
+ if (this.viewer === started)
4330
+ this.viewer = null;
4331
+ });
4332
+ return started;
4333
+ }
4334
+ stopViewer() {
4335
+ void this.viewer?.then((v) => v.child.kill(), () => {
4336
+ });
4337
+ this.viewer = null;
4338
+ }
4339
+ dispose() {
4340
+ this.stopViewer();
4341
+ }
4342
+ };
4343
+ }
4344
+ });
4345
+
4346
+ // ../core/dist/kioku-routes.js
3936
4347
  import { Hono as Hono4 } from "hono";
4348
+ function kiokuRoutes(kioku = new Kioku(), options = {}) {
4349
+ const app = new Hono4();
4350
+ app.onError((err, c) => {
4351
+ if (err instanceof KiokuError)
4352
+ return c.json({ error: err.message }, err.status);
4353
+ throw err;
4354
+ });
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
+ });
4379
+ app.post("/viewer", async (c) => c.json({ address: await kioku.openViewer() }));
4380
+ app.delete("/viewer", (c) => {
4381
+ kioku.stopViewer();
4382
+ return c.json({ ok: true });
4383
+ });
4384
+ return app;
4385
+ }
4386
+ var REMEMBER_MAX;
4387
+ var init_kioku_routes = __esm({
4388
+ "../core/dist/kioku-routes.js"() {
4389
+ "use strict";
4390
+ init_kioku();
4391
+ REMEMBER_MAX = 500;
4392
+ }
4393
+ });
4394
+
4395
+ // ../core/dist/server.js
4396
+ import { Hono as Hono5 } from "hono";
3937
4397
  import { cors } from "hono/cors";
3938
- import { join as join12, basename as basename3, resolve as resolve4 } from "node:path";
3939
- import { readdirSync as readdirSync6, existsSync as existsSync11 } from "node:fs";
4398
+ import { join as join14, basename as basename3, resolve as resolve4 } from "node:path";
4399
+ import { readdirSync as readdirSync6, existsSync as existsSync13 } from "node:fs";
3940
4400
  function createForgeServer(options) {
3941
4401
  const { dataDir, db: externalDb, authToken, localOnly = true } = options;
3942
- const dbPath = join12(dataDir, "forge.db");
3943
- const modulesDir = join12(dataDir, "modules");
4402
+ const dbPath = join14(dataDir, "forge.db");
4403
+ const modulesDir = join14(dataDir, "modules");
3944
4404
  const db = externalDb ?? new ForgeDB(dbPath);
3945
4405
  const loader = new ModuleLoader(modulesDir, options.bundledModulesDir);
3946
4406
  const runner = new ActionRunner();
3947
4407
  loader.discover();
3948
- const app = new Hono4();
4408
+ const app = new Hono5();
3949
4409
  app.use("*", localOnly ? localOriginGuard() : cors());
3950
4410
  if (authToken) {
3951
4411
  app.use("/api/*", bearerAuth(authToken));
@@ -3973,9 +4433,15 @@ function createForgeServer(options) {
3973
4433
  }
3974
4434
  });
3975
4435
  const ptyManager = new PTYManager(states);
3976
- const generalSessions = new GeneralSessions(join12(dataDir, "general-sessions.json"));
4436
+ const generalSessions = new GeneralSessions(join14(dataDir, "general-sessions.json"));
3977
4437
  const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly, general: generalSessions });
3978
4438
  app.route("/api/liveframe", liveframeRoutes());
4439
+ const kioku = new Kioku({ projects: () => cwReader.getProjects() });
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
+ }));
3979
4445
  app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
3980
4446
  app.post("/api/cw/terminal/kill", async (c) => {
3981
4447
  const { project, sessionDir } = await c.req.json();
@@ -4131,21 +4597,21 @@ data: ${JSON.stringify(data)}
4131
4597
  if (!dir.startsWith(home)) {
4132
4598
  return c.json({ error: "Access denied: path must be within home directory" }, 403);
4133
4599
  }
4134
- if (!existsSync11(dir)) {
4600
+ if (!existsSync13(dir)) {
4135
4601
  return c.json({ error: "Directory not found" }, 404);
4136
4602
  }
4137
4603
  try {
4138
4604
  const entries = readdirSync6(dir, { withFileTypes: true });
4139
4605
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
4140
4606
  name: e.name,
4141
- path: join12(dir, e.name),
4142
- hasPackageJson: existsSync11(join12(dir, e.name, "package.json")),
4143
- hasGit: existsSync11(join12(dir, e.name, ".git"))
4607
+ path: join14(dir, e.name),
4608
+ hasPackageJson: existsSync13(join14(dir, e.name, "package.json")),
4609
+ hasGit: existsSync13(join14(dir, e.name, ".git"))
4144
4610
  })).sort((a, b) => a.name.localeCompare(b.name));
4145
4611
  return c.json({
4146
4612
  current: dir,
4147
4613
  name: basename3(dir),
4148
- parent: join12(dir, ".."),
4614
+ parent: join14(dir, ".."),
4149
4615
  directories: dirs
4150
4616
  });
4151
4617
  } catch {
@@ -4163,6 +4629,7 @@ data: ${JSON.stringify(data)}
4163
4629
  ptyManager.dispose();
4164
4630
  states.dispose();
4165
4631
  loginManager.dispose();
4632
+ kioku.dispose();
4166
4633
  db.close();
4167
4634
  }
4168
4635
  };
@@ -4186,6 +4653,8 @@ var init_server = __esm({
4186
4653
  init_login_manager();
4187
4654
  init_general_sessions();
4188
4655
  init_liveframe_routes();
4656
+ init_kioku_routes();
4657
+ init_kioku();
4189
4658
  }
4190
4659
  });
4191
4660
 
@@ -4308,8 +4777,8 @@ async function createDatabase(opts) {
4308
4777
  await db.migrate();
4309
4778
  return db;
4310
4779
  }
4311
- const { join: join16 } = await import("node:path");
4312
- return new ForgeDB(join16(opts.dataDir, "forge.db"));
4780
+ const { join: join17 } = await import("node:path");
4781
+ return new ForgeDB(join17(opts.dataDir, "forge.db"));
4313
4782
  }
4314
4783
  var init_db_factory = __esm({
4315
4784
  "../core/dist/db-factory.js"() {
@@ -4319,22 +4788,22 @@ var init_db_factory = __esm({
4319
4788
  });
4320
4789
 
4321
4790
  // ../core/dist/init.js
4322
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
4323
- import { join as join13 } from "node:path";
4324
- import { homedir as homedir5 } from "node:os";
4791
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
4792
+ import { join as join15 } from "node:path";
4793
+ import { homedir as homedir6 } from "node:os";
4325
4794
  function getForgeDir() {
4326
- return join13(homedir5(), ".forge");
4795
+ return join15(homedir6(), ".forge");
4327
4796
  }
4328
4797
  function ensureForgeDir() {
4329
4798
  const forgeDir = getForgeDir();
4330
- if (existsSync12(forgeDir)) {
4799
+ if (existsSync14(forgeDir)) {
4331
4800
  return { forgeDir, created: false };
4332
4801
  }
4333
4802
  mkdirSync5(forgeDir, { recursive: true });
4334
4803
  for (const d of FORGE_SUBDIRS) {
4335
- mkdirSync5(join13(forgeDir, d), { recursive: true });
4804
+ mkdirSync5(join15(forgeDir, d), { recursive: true });
4336
4805
  }
4337
- writeFileSync4(join13(forgeDir, "config.json"), JSON.stringify({
4806
+ writeFileSync5(join15(forgeDir, "config.json"), JSON.stringify({
4338
4807
  port: 3e3,
4339
4808
  theme: "dark",
4340
4809
  openBrowser: true,
@@ -4350,138 +4819,6 @@ var init_init = __esm({
4350
4819
  }
4351
4820
  });
4352
4821
 
4353
- // ../core/dist/cw-install.js
4354
- import { execFile as execFile5 } from "node:child_process";
4355
- import { createHash as createHash2 } from "node:crypto";
4356
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
4357
- import { delimiter as delimiter2, join as join14 } from "node:path";
4358
- function readCwVersion(file) {
4359
- try {
4360
- const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync9(file, "utf-8"));
4361
- return match ? match[1] : null;
4362
- } catch {
4363
- return null;
4364
- }
4365
- }
4366
- function compareVersions(a, b) {
4367
- const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
4368
- const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
4369
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
4370
- const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
4371
- if (diff !== 0)
4372
- return diff;
4373
- }
4374
- return 0;
4375
- }
4376
- function readCwBundle(dir) {
4377
- try {
4378
- const { ref } = JSON.parse(readFileSync9(join14(dir, ".bundle.json"), "utf-8"));
4379
- const version = readCwVersion(join14(dir, "cw"));
4380
- if (!ref || !version || !existsSync13(join14(dir, "install.sh")))
4381
- return null;
4382
- return { dir, version, ref };
4383
- } catch {
4384
- return null;
4385
- }
4386
- }
4387
- function decideCwInstall({ bundle, installed, marker, cwOnPath, skip }) {
4388
- if (skip)
4389
- return { action: "skip", reason: "FORGE_SKIP_CW_INSTALL is set" };
4390
- if (!bundle)
4391
- return { action: "skip", reason: "this Forge carries no CW" };
4392
- if (!installed) {
4393
- return cwOnPath ? { action: "skip", reason: "using the cw found on PATH" } : { action: "install", reason: "CW is not installed" };
4394
- }
4395
- if (!installed.version)
4396
- return { action: "skip", reason: "cannot read the installed CW version" };
4397
- const newer = compareVersions(bundle.version, installed.version);
4398
- const ownedByForge = marker !== null && marker.sha256 === installed.sha256;
4399
- if (ownedByForge) {
4400
- if (marker.ref === bundle.ref)
4401
- return { action: "skip", reason: `CW ${installed.version} is current` };
4402
- return newer >= 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `installed CW ${installed.version} is newer than ${bundle.version}` };
4403
- }
4404
- return newer > 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `CW ${installed.version} is current` };
4405
- }
4406
- function findOnPath(name, pathVar, excludeDir) {
4407
- for (const dir of (pathVar ?? "").split(delimiter2)) {
4408
- if (!dir || dir === excludeDir)
4409
- continue;
4410
- const candidate = join14(dir, name);
4411
- try {
4412
- if (!statSync3(candidate).isFile())
4413
- continue;
4414
- accessSync2(candidate, constants2.X_OK);
4415
- return candidate;
4416
- } catch {
4417
- }
4418
- }
4419
- return null;
4420
- }
4421
- function pathWithCw(cwHome, pathVar) {
4422
- const bin = join14(cwHome, "bin");
4423
- const rest = (pathVar ?? "").split(delimiter2).filter((dir) => dir && dir !== bin);
4424
- return [bin, ...rest].join(delimiter2);
4425
- }
4426
- function readMarker(cwHome) {
4427
- try {
4428
- return JSON.parse(readFileSync9(join14(cwHome, MARKER_FILE), "utf-8"));
4429
- } catch {
4430
- return null;
4431
- }
4432
- }
4433
- function failureMessage2(output) {
4434
- const lines2 = output.replace(ANSI_RE3, "").split("\n").map((l) => l.trim()).filter(Boolean);
4435
- const errors = lines2.filter((l) => l.startsWith("[\u2717]")).map((l) => l.slice(3).trim());
4436
- return (errors.length > 0 ? errors : lines2.slice(-3)).join(" ") || "the CW installer failed";
4437
- }
4438
- async function ensureCw(options) {
4439
- const { cwHome, bundleDir } = options;
4440
- const env = options.env ?? process.env;
4441
- const run = options.run ?? runInstall;
4442
- const bin = join14(cwHome, "bin", "cw");
4443
- const bundle = readCwBundle(bundleDir);
4444
- const installed = existsSync13(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
4445
- const decision = decideCwInstall({
4446
- bundle,
4447
- installed,
4448
- marker: readMarker(cwHome),
4449
- cwOnPath: findOnPath("cw", env.PATH, join14(cwHome, "bin")) !== null,
4450
- skip: env.FORGE_SKIP_CW_INSTALL === "1"
4451
- });
4452
- if (decision.action === "skip" || !bundle)
4453
- return { action: "skipped", message: decision.reason };
4454
- const { code, output } = await run(join14(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
4455
- if (code !== 0 || !existsSync13(bin))
4456
- return { action: "failed", message: failureMessage2(output) };
4457
- const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
4458
- writeFileSync5(join14(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
4459
- return {
4460
- action: decision.action === "install" ? "installed" : "updated",
4461
- version: bundle.version,
4462
- message: decision.reason
4463
- };
4464
- }
4465
- var MARKER_FILE, INSTALL_TIMEOUT_MS2, ANSI_RE3, sha256Of, runInstall;
4466
- var init_cw_install = __esm({
4467
- "../core/dist/cw-install.js"() {
4468
- "use strict";
4469
- MARKER_FILE = ".forge-cw.json";
4470
- INSTALL_TIMEOUT_MS2 = 12e4;
4471
- ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
4472
- sha256Of = (file) => createHash2("sha256").update(readFileSync9(file)).digest("hex");
4473
- runInstall = (script, args, env) => new Promise((resolve5) => {
4474
- execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
4475
- const output = `${String(stdout)}${String(stderr)}`;
4476
- if (!err)
4477
- return resolve5({ code: 0, output });
4478
- const code = err.code;
4479
- resolve5({ code: typeof code === "number" ? code : 1, output: output || err.message });
4480
- });
4481
- });
4482
- }
4483
- });
4484
-
4485
4822
  // ../core/dist/port-in-use.js
4486
4823
  function waitForListening(server) {
4487
4824
  if (server.listening)
@@ -4537,6 +4874,8 @@ __export(dist_exports, {
4537
4874
  ForgeDB: () => ForgeDB,
4538
4875
  HARNESS_CAPABILITIES: () => HARNESS_CAPABILITIES,
4539
4876
  HARNESS_NAME_RE: () => HARNESS_NAME_RE,
4877
+ Kioku: () => Kioku,
4878
+ KiokuError: () => KiokuError,
4540
4879
  Liveframe: () => Liveframe,
4541
4880
  LiveframeError: () => LiveframeError,
4542
4881
  MODEL_NAME_RE: () => MODEL_NAME_RE,
@@ -4551,11 +4890,14 @@ __export(dist_exports, {
4551
4890
  ensureCw: () => ensureCw,
4552
4891
  ensureForgeDir: () => ensureForgeDir,
4553
4892
  getForgeDir: () => getForgeDir,
4893
+ kiokuRoutes: () => kiokuRoutes,
4554
4894
  liveframeRoutes: () => liveframeRoutes,
4555
4895
  pathWithCw: () => pathWithCw,
4556
4896
  portInUseMessage: () => portInUseMessage,
4557
4897
  probePort: () => probePort,
4558
4898
  quietSqliteWarning: () => quietSqliteWarning,
4899
+ rememberPrompt: () => rememberPrompt,
4900
+ resolveKiokuBin: () => resolveKiokuBin,
4559
4901
  resolveListenOptions: () => resolveListenOptions,
4560
4902
  skillRoutes: () => skillRoutes,
4561
4903
  supports: () => supports,
@@ -4584,14 +4926,16 @@ var init_dist = __esm({
4584
4926
  init_pty_routes();
4585
4927
  init_liveframe_routes();
4586
4928
  init_liveframe();
4929
+ init_kioku_routes();
4930
+ init_kioku();
4587
4931
  init_port_in_use();
4588
4932
  }
4589
4933
  });
4590
4934
 
4591
4935
  // src/index.ts
4592
- import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
4593
- import { homedir as homedir6 } from "node:os";
4594
- import { join as join15 } from "node:path";
4936
+ import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
4937
+ import { homedir as homedir7 } from "node:os";
4938
+ import { join as join16 } from "node:path";
4595
4939
  function parseArgs(argv) {
4596
4940
  const out = { open: true };
4597
4941
  for (let i = 0; i < argv.length; i++) {
@@ -4602,11 +4946,11 @@ function parseArgs(argv) {
4602
4946
  }
4603
4947
  return out;
4604
4948
  }
4605
- var packageRoot = join15(import.meta.dirname, "..");
4606
- var isCheckout = existsSync14(join15(packageRoot, "src"));
4607
- var consoleDist = isCheckout ? join15(packageRoot, "../console/dist") : join15(packageRoot, "console");
4608
- var forgeVersion = JSON.parse(readFileSync10(join15(packageRoot, "package.json"), "utf-8")).version;
4609
- var bundledModulesDir = isCheckout ? join15(packageRoot, "../../modules") : join15(packageRoot, "modules");
4949
+ var packageRoot = join16(import.meta.dirname, "..");
4950
+ var isCheckout = existsSync15(join16(packageRoot, "src"));
4951
+ var consoleDist = isCheckout ? join16(packageRoot, "../console/dist") : join16(packageRoot, "console");
4952
+ var forgeVersion = JSON.parse(readFileSync10(join16(packageRoot, "package.json"), "utf-8")).version;
4953
+ var bundledModulesDir = isCheckout ? join16(packageRoot, "../../modules") : join16(packageRoot, "modules");
4610
4954
  async function main() {
4611
4955
  const { ensureForgeDir: ensureForgeDir2, createForgeServer: createForgeServer2, createDatabase: createDatabase2, resolveListenOptions: resolveListenOptions2, ensureCw: ensureCw2, pathWithCw: pathWithCw2, quietSqliteWarning: quietSqliteWarning2, waitForListening: waitForListening2, probePort: probePort2, portInUseMessage: portInUseMessage2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
4612
4956
  quietSqliteWarning2();
@@ -4615,8 +4959,8 @@ async function main() {
4615
4959
  if (created) {
4616
4960
  console.log("First run \u2014 initialized Forge.");
4617
4961
  }
4618
- const cwHome = join15(process.env.HOME ?? homedir6(), ".cw");
4619
- const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join15(packageRoot, "cw") });
4962
+ const cwHome = join16(process.env.HOME ?? homedir7(), ".cw");
4963
+ const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join16(packageRoot, "cw") });
4620
4964
  if (cw.action === "installed") console.log(` Installed CW ${cw.version} in ${cwHome}`);
4621
4965
  else if (cw.action === "updated") console.log(` Updated ${cw.message}`);
4622
4966
  else if (cw.action === "failed") console.log(` Could not install CW: ${cw.message}`);
@@ -4641,9 +4985,9 @@ async function main() {
4641
4985
  version: forgeVersion
4642
4986
  });
4643
4987
  const { serveStatic } = await import("@hono/node-server/serve-static");
4644
- if (existsSync14(join15(consoleDist, "index.html"))) {
4988
+ if (existsSync15(join16(consoleDist, "index.html"))) {
4645
4989
  server.app.use("/*", serveStatic({ root: consoleDist }));
4646
- const indexHtml = readFileSync10(join15(consoleDist, "index.html"), "utf-8");
4990
+ const indexHtml = readFileSync10(join16(consoleDist, "index.html"), "utf-8");
4647
4991
  server.app.get("*", (c) => c.html(indexHtml));
4648
4992
  }
4649
4993
  const { serve } = await import("@hono/node-server");