my-durable 0.0.29 → 0.0.31
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/apps/dpi-backend/src/faux.ts +6 -0
- package/apps/dpi-backend/src/git.ts +57 -2
- package/apps/dpi-backend/src/routes.ts +12 -0
- package/apps/dpi-backend/src/store.ts +91 -1
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-B2OcBmds.js +81 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CL5-3stf.css +1 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +2 -2
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/GitPanel.vue +287 -32
- package/apps/dpi-sdk/src/client.ts +18 -1
- package/apps/dpi-sdk/src/protocol/rest-routes.ts +1 -0
- package/package.json +1 -1
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-C325mnAU.css +0 -1
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CWO1e3Gk.js +0 -79
|
@@ -67,6 +67,12 @@ export function installFaux(models: MutableModels) {
|
|
|
67
67
|
const ref = prompt.slice(6).trim();
|
|
68
68
|
return call("check_durable_job", /^\d+$/.test(ref) ? { task_id: Number(ref) } : ref ? { job: ref } : {});
|
|
69
69
|
}
|
|
70
|
+
if (prompt.startsWith("write:")) {
|
|
71
|
+
// "write:<path>|<content>" — exercises the real write tool so git-panel
|
|
72
|
+
// features (session changelist, per-turn summary) are testable.
|
|
73
|
+
const [path, ...rest] = prompt.slice(6).split("|");
|
|
74
|
+
return call("write", { path: (path ?? "").trim(), content: rest.join("|") });
|
|
75
|
+
}
|
|
70
76
|
if (prompt.startsWith("search:")) return call("search_history", { query: prompt.slice(7).trim() });
|
|
71
77
|
if (prompt.startsWith("page:")) return call("ping_on_call_engineer", { summary: prompt.slice(5).trim() });
|
|
72
78
|
if (prompt.startsWith("Release check:"))
|
|
@@ -41,6 +41,18 @@ export function git(cwd: string, args: string[], timeout = GIT_TIMEOUT): Promise
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** One GitHub CLI call — PRs, checks and comments are forge territory and gh
|
|
45
|
+
* is the least-token-ceremony way to reach them from a device. Same no-shell
|
|
46
|
+
* rule: argv only, plain text back. */
|
|
47
|
+
export function gh(cwd: string, args: string[], timeout = GIT_TIMEOUT): Promise<string> {
|
|
48
|
+
return new Promise((ok, fail) => {
|
|
49
|
+
execFile("gh", ["--repo", cwd, ...args], { timeout, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
50
|
+
if (error) fail(Error(String(stderr || stdout || error.message).trim().slice(0, 400)));
|
|
51
|
+
else ok(String(stdout));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
const count = (s: string, re: RegExp) => (s.match(re) ?? []).length;
|
|
45
57
|
|
|
46
58
|
export interface GitStatus {
|
|
@@ -230,7 +242,13 @@ export type GitOp =
|
|
|
230
242
|
| "ignore"
|
|
231
243
|
| "checkpoint"
|
|
232
244
|
| "checkpoints"
|
|
233
|
-
| "rewind"
|
|
245
|
+
| "rewind"
|
|
246
|
+
| "submodule-update"
|
|
247
|
+
| "pr-create"
|
|
248
|
+
| "pr-list"
|
|
249
|
+
| "pr-merge"
|
|
250
|
+
| "pr-checks"
|
|
251
|
+
| "pr-comment";
|
|
234
252
|
|
|
235
253
|
/** Plain string an arg may never contain — we build argv ourselves anyway. */
|
|
236
254
|
const safeRef = (s: string) => /^[\w./@+-]+$/.test(s) && !s.includes("..");
|
|
@@ -255,6 +273,10 @@ export async function gitOp(
|
|
|
255
273
|
name?: string;
|
|
256
274
|
/** ignore: pattern lines to append to .gitignore. */
|
|
257
275
|
lines?: string[];
|
|
276
|
+
/** pr: merge method + comment body + PR number. */
|
|
277
|
+
method?: "squash" | "merge";
|
|
278
|
+
body?: string;
|
|
279
|
+
n?: number;
|
|
258
280
|
} = {},
|
|
259
281
|
): Promise<{
|
|
260
282
|
text: string;
|
|
@@ -263,8 +285,9 @@ export async function gitOp(
|
|
|
263
285
|
behind?: number;
|
|
264
286
|
changed?: { status: string; path: string }[];
|
|
265
287
|
checkpoints?: { name: string; hash: string; date: string; subject: string }[];
|
|
288
|
+
prs?: { n: number; title: string; state: string; draft: boolean; branch: string; url: string }[];
|
|
266
289
|
}> {
|
|
267
|
-
const { ref, message, target, branch, files, create, staged, rebase, setUpstream, name, lines } = opts;
|
|
290
|
+
const { ref, message, target, branch, files, create, staged, rebase, setUpstream, name, lines, method, body, n } = opts;
|
|
268
291
|
switch (op) {
|
|
269
292
|
case "diff":
|
|
270
293
|
return { text: await git(path, ["--no-pager", "diff", ...(ref && safeRef(ref) ? [ref] : [])]) };
|
|
@@ -412,6 +435,38 @@ export async function gitOp(
|
|
|
412
435
|
const sha = (await git(path, ["rev-parse", "--verify", refname])).trim();
|
|
413
436
|
return { text: (await git(path, ["reset", "--hard", sha])).trim().split("\n")[0] ?? `rewound to ${name}` };
|
|
414
437
|
}
|
|
438
|
+
case "submodule-update":
|
|
439
|
+
return { text: (await git(path, ["submodule", "update", "--init", "--recursive"], GIT_SLOW_TIMEOUT)).trim() || "submodules up to date" };
|
|
440
|
+
case "pr-create": {
|
|
441
|
+
// gh pr create — title from message, head = current branch. gh owns auth.
|
|
442
|
+
const title = (message ?? "").trim();
|
|
443
|
+
if (!title) throw Error("pr-create needs a title (message)");
|
|
444
|
+
const args = ["pr", "create", "--title", title.slice(0, 200), "--body", (body ?? title).slice(0, 4000)];
|
|
445
|
+
if (ref && safeRef(ref)) args.push("--base", ref);
|
|
446
|
+
return { text: (await gh(path, args, GIT_SLOW_TIMEOUT)).trim() };
|
|
447
|
+
}
|
|
448
|
+
case "pr-list": {
|
|
449
|
+
const raw = await gh(path, ["pr", "list", "--limit", "20", "--json", "number,title,state,isDraft,headRefName,url"]);
|
|
450
|
+
const list = JSON.parse(raw || "[]") as { number: number; title: string; state: string; isDraft: boolean; headRefName: string; url: string }[];
|
|
451
|
+
return {
|
|
452
|
+
text: "",
|
|
453
|
+
prs: list.map((r) => ({ n: r.number, title: r.title, state: r.state, draft: r.isDraft, branch: r.headRefName, url: r.url })),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
case "pr-merge": {
|
|
457
|
+
if (!n || !Number.isInteger(n)) throw Error("pr-merge needs a PR number");
|
|
458
|
+
return { text: (await gh(path, ["pr", "merge", String(n), method === "merge" ? "--merge" : "--squash"], GIT_SLOW_TIMEOUT)).trim() || `PR #${n} merged` };
|
|
459
|
+
}
|
|
460
|
+
case "pr-checks": {
|
|
461
|
+
if (!n || !Number.isInteger(n)) throw Error("pr-checks needs a PR number");
|
|
462
|
+
return { text: (await gh(path, ["pr", "checks", String(n)], GIT_SLOW_TIMEOUT)).trim() || "no checks reported" };
|
|
463
|
+
}
|
|
464
|
+
case "pr-comment": {
|
|
465
|
+
if (!n || !Number.isInteger(n)) throw Error("pr-comment needs a PR number");
|
|
466
|
+
const text = (body ?? "").trim();
|
|
467
|
+
if (!text) throw Error("pr-comment needs a body");
|
|
468
|
+
return { text: (await gh(path, ["pr", "comment", String(n), "--body", text.slice(0, 4000)])).trim() };
|
|
469
|
+
}
|
|
415
470
|
case "commit": {
|
|
416
471
|
const msg = (message ?? "").trim();
|
|
417
472
|
if (!msg) throw Error("commit needs a message");
|
|
@@ -112,6 +112,13 @@ export function v1(
|
|
|
112
112
|
|
|
113
113
|
// ---------------------------------------------------------- device
|
|
114
114
|
r.get("/device", (_req, res) => res.json(store.deviceInfo()));
|
|
115
|
+
// Model health, probed live: one provider, its first catalog model, one
|
|
116
|
+
// throwaway session. Local control plane only — deliberately NOT tunnelled.
|
|
117
|
+
r.post("/device/probe-model", route(async (req, res) => {
|
|
118
|
+
const provider = String(req.body?.provider ?? "");
|
|
119
|
+
const modelId = String(req.body?.modelId ?? "");
|
|
120
|
+
res.json(await store.probeModel(provider, modelId));
|
|
121
|
+
}));
|
|
115
122
|
r.get("/tree", route(async (_req, res) => res.json(await store.tree())));
|
|
116
123
|
r.get("/events", (req, res, next) => {
|
|
117
124
|
try {
|
|
@@ -145,6 +152,9 @@ export function v1(
|
|
|
145
152
|
setUpstream: body.setUpstream === true,
|
|
146
153
|
name: typeof body.name === "string" ? body.name : undefined,
|
|
147
154
|
lines: Array.isArray(body.lines) ? body.lines.map(String).slice(0, 50) : undefined,
|
|
155
|
+
method: body.method === "merge" ? "merge" : "squash",
|
|
156
|
+
body: typeof body.body === "string" ? body.body : undefined,
|
|
157
|
+
n: Number.isInteger(body.n) ? Number(body.n) : undefined,
|
|
148
158
|
});
|
|
149
159
|
res.json(out);
|
|
150
160
|
}));
|
|
@@ -169,6 +179,8 @@ export function v1(
|
|
|
169
179
|
// The zcode flagship, adapted: which files has THIS session's agent touched?
|
|
170
180
|
// Powers "commit session changes" in the git panel.
|
|
171
181
|
r.get("/sessions/:s/git-files", route(async (_req, res) => res.json({ files: await store.sessionTouchedFiles(L(res).row) })));
|
|
182
|
+
// Per-turn breakdown of the same data (zcode taskChangeSummary shape).
|
|
183
|
+
r.get("/sessions/:s/git-turns", route(async (_req, res) => res.json({ turns: await store.sessionGitTurns(L(res).row) })));
|
|
172
184
|
r.get(
|
|
173
185
|
"/sessions/:s/tasks",
|
|
174
186
|
route(async (req, res) => {
|
|
@@ -301,7 +301,15 @@ export class Store {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
private defaultModel() {
|
|
304
|
-
|
|
304
|
+
if (this.options.provider === "faux") return fauxModels[0].id;
|
|
305
|
+
// The curated constant can drift out of the LIVE catalog (seen: default
|
|
306
|
+
// anthropic/claude-sonnet-5.5 while the credential store held 41 other
|
|
307
|
+
// models) — never point the default outside the catalog we serve.
|
|
308
|
+
// (this.modelList, NOT deviceInfo() — that would recurse.)
|
|
309
|
+
const catalog = this.modelList;
|
|
310
|
+
return catalog.some((m) => `${m.provider}/${m.id}` === defaultModelId)
|
|
311
|
+
? defaultModelId
|
|
312
|
+
: (catalog[0] ? `${catalog[0].provider}/${catalog[0].id}` : defaultModelId);
|
|
305
313
|
}
|
|
306
314
|
|
|
307
315
|
/** True when the device has no provider key: the catalog shown is the built-in
|
|
@@ -761,6 +769,88 @@ export class Store {
|
|
|
761
769
|
return [...paths];
|
|
762
770
|
}
|
|
763
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Per-turn file summary (zcode taskChangeSummary): every user turn of the
|
|
774
|
+
* session, with the files its agents wrote or edited during that turn.
|
|
775
|
+
* Line stats come from the UI against the live status — here we only know
|
|
776
|
+
* WHAT was touched and how many times.
|
|
777
|
+
*/
|
|
778
|
+
async sessionGitTurns(row: SessionRow): Promise<{ when: string; files: { path: string; writes: number }[] }[]> {
|
|
779
|
+
const h = await this.openAgent(row);
|
|
780
|
+
const scanned: { when: string; files: Map<string, number> }[] = [];
|
|
781
|
+
let current: { when: string; files: Map<string, number> } | null = null;
|
|
782
|
+
const fileOf = (b: { type?: string; name?: string; arguments?: Record<string, unknown> }): string[] => {
|
|
783
|
+
if (b?.type !== "toolCall" || (b.name !== "write" && b.name !== "edit")) return [];
|
|
784
|
+
const args = b.arguments ?? {};
|
|
785
|
+
const out: string[] = [];
|
|
786
|
+
for (const f of [args.file_path, args.path]) if (typeof f === "string" && f.length > 0 && f.length < 4096) out.push(f);
|
|
787
|
+
const edits = Array.isArray(args.edits) ? (args.edits as { path?: unknown }[]) : [];
|
|
788
|
+
for (const ed of edits) if (typeof ed.path === "string" && ed.path.length > 0) out.push(ed.path);
|
|
789
|
+
return out;
|
|
790
|
+
};
|
|
791
|
+
await h.commit(async (tx) => {
|
|
792
|
+
let convs = await tx.scanConversations({}, 1000);
|
|
793
|
+
const records = [...convs.items];
|
|
794
|
+
while (convs.next && records.length < 100) {
|
|
795
|
+
convs = await tx.scanConversations({}, 1000, convs.next);
|
|
796
|
+
records.push(...convs.items);
|
|
797
|
+
}
|
|
798
|
+
for (const c of records) {
|
|
799
|
+
// ascending: a turn's tool calls FOLLOW its user entry (default is
|
|
800
|
+
// descending, which attributes every call to the NEXT turn — caught
|
|
801
|
+
// by the git-session-files test).
|
|
802
|
+
let page = await tx.scanEntries({ conversationId: c.id, order: "ascending" }, 500);
|
|
803
|
+
let count = 0;
|
|
804
|
+
for (;;) {
|
|
805
|
+
for (const e of page.items as unknown as { kind?: string; model?: readonly { content?: unknown }[] }[]) {
|
|
806
|
+
if (e.kind === "pi.user") {
|
|
807
|
+
current = { when: new Date().toISOString(), files: new Map() };
|
|
808
|
+
scanned.push(current);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (!current) continue;
|
|
812
|
+
for (const m of e.model ?? []) {
|
|
813
|
+
const blocks = Array.isArray(m.content) ? (m.content as unknown[]) : [];
|
|
814
|
+
for (const b of blocks as { type?: string; name?: string; arguments?: Record<string, unknown> }[]) {
|
|
815
|
+
for (const f of fileOf(b)) current.files.set(f, (current.files.get(f) ?? 0) + 1);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
count += page.items.length;
|
|
820
|
+
if (!page.next || count >= 4000) break;
|
|
821
|
+
page = await tx.scanEntries({ conversationId: c.id }, 500, page.next);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}, ctx);
|
|
825
|
+
return scanned
|
|
826
|
+
.map((t) => ({ when: t.when, files: [...t.files.entries()].map(([path, writes]) => ({ path, writes })).slice(0, 50) }))
|
|
827
|
+
.slice(-20);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** One provider, one model, one throwaway session — is it alive? */
|
|
831
|
+
async probeModel(provider: string, modelId: string): Promise<{ ok: boolean; detail: string }> {
|
|
832
|
+
if (!provider || !modelId) throw invalid("provider and modelId are required");
|
|
833
|
+
const created: SessionRow[] = [];
|
|
834
|
+
try {
|
|
835
|
+
const summary = await this.create({ name: `probe-${provider}`, cwd: this.startDirs[0] ?? homedir(), model: { provider, modelId } });
|
|
836
|
+
const row = { ...summary, createdAt: Number(summary.createdAt) } as unknown as SessionRow;
|
|
837
|
+
created.push(row);
|
|
838
|
+
const h = await this.openAgent(row);
|
|
839
|
+
const c = (await h.conversation(1 as ConversationId, ctx))!;
|
|
840
|
+
const submission = await this.send(c, { content: "Reply with exactly: ok", whenBusy: "followUp", requestId: `probe-${Date.now()}` });
|
|
841
|
+
const result = await this.settled(h, submission.id as number, 45_000);
|
|
842
|
+
const text = result.status === "done" ? result.answer.text.trim() : "";
|
|
843
|
+
return {
|
|
844
|
+
ok: result.status === "done" && !!text,
|
|
845
|
+
detail: result.status === "done" ? (text || "empty answer").slice(0, 120) : ((result as { reason?: string }).reason ?? "no answer").slice(0, 120),
|
|
846
|
+
};
|
|
847
|
+
} catch (e) {
|
|
848
|
+
return { ok: false, detail: String((e as Error).message).slice(0, 160) };
|
|
849
|
+
} finally {
|
|
850
|
+
for (const r of created) void this.archive(r).catch(() => {});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
764
854
|
// ------------------------------------------------------------ agents
|
|
765
855
|
|
|
766
856
|
private names(sessionId: string) {
|