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.
- package/console/assets/index-5ifgSOpU.js +41 -0
- package/console/index.html +1 -1
- package/dist/index.js +284 -36
- package/package.json +1 -1
- package/console/assets/index-BpkdtYRb.js +0 -36
package/console/index.html
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
1542
|
+
constructor(cwBin, spawn9 = spawnInPty) {
|
|
1530
1543
|
this.cwBin = cwBin;
|
|
1531
|
-
this.spawn =
|
|
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
|
|
1816
|
-
const harness =
|
|
1817
|
-
const provider =
|
|
1818
|
-
const model =
|
|
1819
|
-
if (!
|
|
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 =
|
|
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
|
|
2887
|
-
const source =
|
|
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(
|
|
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
|
|
2900
|
-
const 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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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("
|
|
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
|
|
4465
|
-
for (const [key, value] of Object.entries(
|
|
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
|
|
4734
|
-
return
|
|
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,
|