@monster-orange/den 0.4.26 → 0.4.28
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/CHANGELOG.md +88 -0
- package/docs/distributions.md +145 -20
- package/package.json +1 -1
- package/src/shared/agents/claude-helpers.ts +623 -0
- package/src/shared/agents/den-claude.ts +54 -0
- package/src/shared/agents/helpers.ts +175 -0
- package/src/shared/agents/runtime.ts +15 -2
- package/src/shared/den-config.ts +42 -1
- package/src/shared/hub.ts +314 -82
- package/src/shared/secrets/onepassword.ts +52 -15
- package/src/ui/cli/agent-ops.ts +161 -3
- package/src/ui/cli/agents-command.ts +127 -1
- package/src/ui/cli/doctor-command.ts +11 -2
- package/src/ui/mcp/api.ts +8 -2
- package/src/ui/mcp/server.ts +13 -1
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-routes.ts +133 -1
- package/src/ui/server/hub-connect.ts +499 -0
- package/src/ui/server/index.ts +7 -1
- package/src/ui/server/public/agent-helpers.js +311 -0
- package/src/ui/server/public/agents-page.js +76 -9
- package/src/ui/server/public/i18n/es.js +41 -0
- package/src/ui/server/public/i18n/ja.js +39 -0
- package/src/ui/server/public/settings-registry.js +232 -33
- package/src/ui/server/public/settings.html +23 -0
- package/src/ui/server/public/shell.js +201 -0
- package/src/ui/server/public/topbar.js +5 -1
- package/src/ui/server/public/transcript-view.js +202 -0
- package/src/ui/server/request-guard.ts +3 -1
- package/src/ui/server/route-classes.ts +33 -0
- package/src/ui/server/session-routes.ts +71 -1
- package/src/ui/server/ui-assets.ts +3 -0
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -50,6 +50,15 @@
|
|
|
50
50
|
* Nothing here takes a grant: grants come from the org config only. The
|
|
51
51
|
* host enforces agents.max_concurrent on every start, counting agents only.
|
|
52
52
|
*/
|
|
53
|
+
import {
|
|
54
|
+
capHelpers,
|
|
55
|
+
HELPER_DEFAULTS,
|
|
56
|
+
HELPERS_READ_ONLY_NOTE,
|
|
57
|
+
helperCounts,
|
|
58
|
+
type Helper,
|
|
59
|
+
type HelperSettings,
|
|
60
|
+
type HelperTranscript,
|
|
61
|
+
} from "../../shared/agents/helpers.js";
|
|
53
62
|
import * as fs from "fs";
|
|
54
63
|
import * as path from "path";
|
|
55
64
|
import {
|
|
@@ -265,6 +274,8 @@ export interface AgentsDeps extends SessionsDeps {
|
|
|
265
274
|
url: string;
|
|
266
275
|
token: string;
|
|
267
276
|
allowTools?: boolean;
|
|
277
|
+
/** HUB-1.7: Hub's MCP endpoint (default <url>/mcp). */
|
|
278
|
+
mcpUrl?: string;
|
|
268
279
|
} | null>;
|
|
269
280
|
/** HUB-1.5: workspace `ws`'s repos as owner/repo (X-Den-Repos). */
|
|
270
281
|
workspaceRepos?: (ws: string) => string[];
|
|
@@ -288,6 +299,7 @@ export interface AgentsResolved extends Resolved {
|
|
|
288
299
|
url: string;
|
|
289
300
|
token: string;
|
|
290
301
|
allowTools?: boolean;
|
|
302
|
+
mcpUrl?: string;
|
|
291
303
|
} | null>;
|
|
292
304
|
workspaceRepos: (ws: string) => string[];
|
|
293
305
|
}
|
|
@@ -395,6 +407,7 @@ export async function agentHub(
|
|
|
395
407
|
d: AgentsResolved,
|
|
396
408
|
ws: string,
|
|
397
409
|
name: string,
|
|
410
|
+
agentId?: string,
|
|
398
411
|
): Promise<AgentMcp["hub"] | undefined> {
|
|
399
412
|
const h = await d.hub().catch(() => null);
|
|
400
413
|
if (!h) return undefined;
|
|
@@ -407,10 +420,11 @@ export async function agentHub(
|
|
|
407
420
|
}
|
|
408
421
|
return {
|
|
409
422
|
name: HUB_SERVER,
|
|
410
|
-
url: h.url
|
|
423
|
+
url: h.mcpUrl || `${h.url}/mcp`,
|
|
411
424
|
allowTools: h.allowTools !== false,
|
|
412
425
|
headers: hubHeaders(h.token, {
|
|
413
426
|
agent: name,
|
|
427
|
+
...(agentId ? { agentId } : {}),
|
|
414
428
|
session: crypto.randomUUID(),
|
|
415
429
|
workspace: ws,
|
|
416
430
|
repos,
|
|
@@ -480,6 +494,7 @@ async function defaultHub(): Promise<{
|
|
|
480
494
|
url: string;
|
|
481
495
|
token: string;
|
|
482
496
|
allowTools: boolean;
|
|
497
|
+
mcpUrl: string;
|
|
483
498
|
} | null> {
|
|
484
499
|
const { readHubConfig, readHubToken } = await import("../../shared/hub.js");
|
|
485
500
|
let cfg;
|
|
@@ -490,7 +505,9 @@ async function defaultHub(): Promise<{
|
|
|
490
505
|
}
|
|
491
506
|
if (!cfg) return null;
|
|
492
507
|
const token = await readHubToken(cfg);
|
|
493
|
-
return token
|
|
508
|
+
return token
|
|
509
|
+
? { url: cfg.url, token, allowTools: cfg.allowTools, mcpUrl: cfg.mcpUrl }
|
|
510
|
+
: null;
|
|
494
511
|
}
|
|
495
512
|
|
|
496
513
|
/**
|
|
@@ -630,6 +647,12 @@ export interface AgentRow {
|
|
|
630
647
|
/** D28: the last one (when, why, the old conversation, by itself or not). */
|
|
631
648
|
lastRotation?: RotationEntry;
|
|
632
649
|
session?: SessionInfo;
|
|
650
|
+
/**
|
|
651
|
+
* D32: the helpers it started in its own session: how many, how many
|
|
652
|
+
* run now, and the ones worth showing under it (running, or ended
|
|
653
|
+
* within agents.helpers.collapse_done_minutes; at most 8).
|
|
654
|
+
*/
|
|
655
|
+
helpers?: { running: number; total: number; recent: Helper[] };
|
|
633
656
|
}
|
|
634
657
|
|
|
635
658
|
/** Its state as a row shows it (AG-14.1: stopped, exited or interrupted). */
|
|
@@ -823,9 +846,138 @@ export async function listAgents(
|
|
|
823
846
|
const records = listRecords(d.stateDir()).filter((r) => r.workspace === ws);
|
|
824
847
|
noteEnded(d.stateDir(), sessions, records);
|
|
825
848
|
const rows = agentRows(sessions, records, d.stateDir(), policyResolver(d));
|
|
849
|
+
addHelperCounts(d, rows, records);
|
|
826
850
|
return opts.stopped ? rows.filter((r) => isStoppedState(r.state)) : rows;
|
|
827
851
|
}
|
|
828
852
|
|
|
853
|
+
/** At most this many helpers ride along on an agent's row. */
|
|
854
|
+
const ROW_HELPERS = 8;
|
|
855
|
+
|
|
856
|
+
/** D32: agents.helpers, or its defaults when the config can't be read. */
|
|
857
|
+
function helperSettings(d: AgentsResolved): HelperSettings {
|
|
858
|
+
try {
|
|
859
|
+
return d.agentsConfig().helpers ?? HELPER_DEFAULTS;
|
|
860
|
+
} catch {
|
|
861
|
+
return HELPER_DEFAULTS;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** D32: each row's helper counts and recent helpers (never fails a list). */
|
|
866
|
+
function addHelperCounts(
|
|
867
|
+
d: AgentsResolved,
|
|
868
|
+
rows: AgentRow[],
|
|
869
|
+
records: AgentRecord[],
|
|
870
|
+
): void {
|
|
871
|
+
const settings = helperSettings(d);
|
|
872
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
873
|
+
for (const row of rows) {
|
|
874
|
+
const rec = byId.get(row.agent);
|
|
875
|
+
if (!rec) continue;
|
|
876
|
+
const h = rowHelpers(rec, !isStoppedState(row.state), settings, d.env);
|
|
877
|
+
if (h) row.helpers = h;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* D32: what an agent's row shows of its helpers: counts, and the ones
|
|
883
|
+
* worth showing (running, or this conversation's ended within
|
|
884
|
+
* collapse_done_minutes; at most 8). Undefined: none, or its runtime
|
|
885
|
+
* can't tell. Never throws.
|
|
886
|
+
*/
|
|
887
|
+
export function rowHelpers(
|
|
888
|
+
rec: AgentRecord,
|
|
889
|
+
live: boolean,
|
|
890
|
+
settings: HelperSettings = HELPER_DEFAULTS,
|
|
891
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
892
|
+
now = Date.now(),
|
|
893
|
+
): AgentRow["helpers"] {
|
|
894
|
+
const rt = getRuntime(rec.runtime);
|
|
895
|
+
if (!rt?.helpers) return undefined;
|
|
896
|
+
let hs: Helper[] = [];
|
|
897
|
+
try {
|
|
898
|
+
hs = rt.helpers(rec, { env, now, settings, agentLive: live });
|
|
899
|
+
} catch {
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
if (!hs.length) return undefined;
|
|
903
|
+
const keep = settings.collapseDoneMinutes * 60_000;
|
|
904
|
+
const shown = hs.filter(
|
|
905
|
+
(h) =>
|
|
906
|
+
h.status === "running" ||
|
|
907
|
+
(!h.earlier && now - (h.lastActivityAt ?? h.startedAt ?? 0) <= keep),
|
|
908
|
+
);
|
|
909
|
+
return { ...helperCounts(hs), recent: capHelpers(shown, ROW_HELPERS) };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** D32: an agent's helpers, read-only, and whether its runtime can tell. */
|
|
913
|
+
export async function agentHelpers(
|
|
914
|
+
d: AgentsResolved,
|
|
915
|
+
id: string,
|
|
916
|
+
ws?: string,
|
|
917
|
+
): Promise<{
|
|
918
|
+
rec: AgentRecord;
|
|
919
|
+
supported: boolean;
|
|
920
|
+
live: boolean;
|
|
921
|
+
helpers: Helper[];
|
|
922
|
+
note: string;
|
|
923
|
+
}> {
|
|
924
|
+
const rec = agentRecord(d, id, ws);
|
|
925
|
+
const rt = getRuntime(rec.runtime);
|
|
926
|
+
const live = await agentLive(d, rec);
|
|
927
|
+
const helpers =
|
|
928
|
+
rt?.helpers?.(rec, {
|
|
929
|
+
env: d.env,
|
|
930
|
+
settings: helperSettings(d),
|
|
931
|
+
agentLive: live,
|
|
932
|
+
}) ?? [];
|
|
933
|
+
return {
|
|
934
|
+
rec,
|
|
935
|
+
supported: !!rt?.helpers,
|
|
936
|
+
live,
|
|
937
|
+
helpers,
|
|
938
|
+
note: HELPERS_READ_ONLY_NOTE,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** D32: one of agent `id`'s helpers' transcript; a 404 when there's none. */
|
|
943
|
+
export async function agentHelperTranscript(
|
|
944
|
+
d: AgentsResolved,
|
|
945
|
+
id: string,
|
|
946
|
+
helperId: string,
|
|
947
|
+
ws?: string,
|
|
948
|
+
limit?: number,
|
|
949
|
+
): Promise<HelperTranscript & { note: string }> {
|
|
950
|
+
const rec = agentRecord(d, id, ws);
|
|
951
|
+
const rt = getRuntime(rec.runtime);
|
|
952
|
+
const t = rt?.helperTranscript?.(rec, helperId, {
|
|
953
|
+
env: d.env,
|
|
954
|
+
settings: helperSettings(d),
|
|
955
|
+
agentLive: await agentLive(d, rec),
|
|
956
|
+
...(limit ? { limit } : {}),
|
|
957
|
+
});
|
|
958
|
+
if (!t) throw refused(`agent ${id} has no helper "${helperId}"`, [], 404);
|
|
959
|
+
return { ...t, note: HELPERS_READ_ONLY_NOTE };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** Whether agent `rec`'s session runs now (an unreachable host: no). */
|
|
963
|
+
async function agentLive(
|
|
964
|
+
d: AgentsResolved,
|
|
965
|
+
rec: AgentRecord,
|
|
966
|
+
): Promise<boolean> {
|
|
967
|
+
try {
|
|
968
|
+
const c = await d.connectExisting();
|
|
969
|
+
if (!c) return false;
|
|
970
|
+
try {
|
|
971
|
+
const s = sessionOf(await c.list(), rec);
|
|
972
|
+
return Boolean(s && !s.exited);
|
|
973
|
+
} finally {
|
|
974
|
+
c.close();
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
829
981
|
// ── personas, runtimes ──────────────────────────────────────────────────────
|
|
830
982
|
|
|
831
983
|
/** The agents config; an invalid one is a refusal naming the key. */
|
|
@@ -1901,6 +2053,7 @@ export async function startAgent(
|
|
|
1901
2053
|
d,
|
|
1902
2054
|
ws,
|
|
1903
2055
|
given ?? prev?.displayName ?? persona.displayName ?? id,
|
|
2056
|
+
id,
|
|
1904
2057
|
);
|
|
1905
2058
|
const input = {
|
|
1906
2059
|
...base,
|
|
@@ -2443,7 +2596,12 @@ export async function resumeAgent(
|
|
|
2443
2596
|
// Resume in the cwd the agent had, wherever the workspace points now.
|
|
2444
2597
|
// D24 (and D25: a rename since) the name peers see it by.
|
|
2445
2598
|
const peerName = peerSessionName(d.cli, rec.workspace, rec);
|
|
2446
|
-
const hub = await agentHub(
|
|
2599
|
+
const hub = await agentHub(
|
|
2600
|
+
d,
|
|
2601
|
+
rec.workspace,
|
|
2602
|
+
rec.displayName ?? rec.id,
|
|
2603
|
+
rec.id,
|
|
2604
|
+
);
|
|
2447
2605
|
const at = {
|
|
2448
2606
|
...input,
|
|
2449
2607
|
...(hub ? { mcp: { ...input.mcp, hub } } : {}),
|
|
@@ -70,6 +70,7 @@ import { HostError } from "../../shared/sessions/client.js";
|
|
|
70
70
|
import { checkHandback } from "../../shared/agents/handback.js";
|
|
71
71
|
import { checkHandoff } from "../../shared/agents/handoff.js";
|
|
72
72
|
import { handoffFile, reasonText } from "../../shared/agents/rotation.js";
|
|
73
|
+
import { shownDepths } from "../../shared/agents/helpers.js";
|
|
73
74
|
import {
|
|
74
75
|
parseRotateValue,
|
|
75
76
|
type OverrideScope,
|
|
@@ -84,6 +85,8 @@ import {
|
|
|
84
85
|
importAgent,
|
|
85
86
|
changePersona,
|
|
86
87
|
listAgents,
|
|
88
|
+
agentHelpers,
|
|
89
|
+
agentHelperTranscript,
|
|
87
90
|
listConversations,
|
|
88
91
|
listPersonas as personaList,
|
|
89
92
|
listRuntimes,
|
|
@@ -129,7 +132,7 @@ export {
|
|
|
129
132
|
|
|
130
133
|
export const AGENTS_HELP = {
|
|
131
134
|
usage:
|
|
132
|
-
"agents <list|start|stop|resume|relaunch|rotate|keep|handoff|import|persona|rename|rm|personas|doctor|handback-check|handoff-check> ...",
|
|
135
|
+
"agents <list|start|stop|resume|relaunch|rotate|keep|handoff|import|persona|helpers|rename|rm|personas|doctor|handback-check|handoff-check> ...",
|
|
133
136
|
lines: [
|
|
134
137
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
135
138
|
"outlives the web server (see `{cli} sessions`).",
|
|
@@ -167,6 +170,13 @@ export const AGENTS_HELP = {
|
|
|
167
170
|
" agent never rotates. A person's only",
|
|
168
171
|
"{cli} agents handoff <id> [-w <ws>]",
|
|
169
172
|
" ask a running agent, at idle, to write its handoff",
|
|
173
|
+
"{cli} agents helpers <id> [-w <ws>] [--all] [--json]",
|
|
174
|
+
" the helpers it started in its own session (Claude Code's Agent and",
|
|
175
|
+
" Workflow subagents): what each does, its type, model, branch, state and",
|
|
176
|
+
" times; running first. --all: its earlier conversations' too. Read-only:",
|
|
177
|
+
" they belong to the agent's session, so Den can't stop or steer them",
|
|
178
|
+
"{cli} agents helpers <id> <helper> [-w <ws>] [--limit <n>] [--json]",
|
|
179
|
+
" one helper's transcript, tool calls summarized (the last 500 lines)",
|
|
170
180
|
"{cli} agents import -w <ws> [--runtime <r>] [--json]",
|
|
171
181
|
"{cli} agents import -w <ws> --session <id> --persona <p> [--runtime <r>] [--id <id>] [--model <m>]",
|
|
172
182
|
" bring in a conversation started outside Den: the first lists the runtime's",
|
|
@@ -456,6 +466,120 @@ async function persona(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
456
466
|
}
|
|
457
467
|
|
|
458
468
|
/** `<id> [-w <ws>]`. */
|
|
469
|
+
/** D32: `agents helpers <id> [<helper>]`, read-only. */
|
|
470
|
+
async function helpers(args: string[], d: AgentsResolved): Promise<number> {
|
|
471
|
+
const where = "agents helpers";
|
|
472
|
+
const p = parseArgs(where, args, {
|
|
473
|
+
"--workspace": true,
|
|
474
|
+
"--json": false,
|
|
475
|
+
"--all": false,
|
|
476
|
+
"--limit": true,
|
|
477
|
+
});
|
|
478
|
+
noExtra(where, p, 2);
|
|
479
|
+
const [id, helperId] = p.positional;
|
|
480
|
+
if (!id) throw new UsageError(`${where} needs <id>`);
|
|
481
|
+
const ws = workspaceFlag(where, p);
|
|
482
|
+
const ago = (ms?: number) =>
|
|
483
|
+
ms === undefined ? "-" : `${duration(Date.now() - ms)} ago`;
|
|
484
|
+
if (helperId) {
|
|
485
|
+
const raw = p.flags["--limit"];
|
|
486
|
+
const limit = raw === undefined ? undefined : Number(raw);
|
|
487
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
|
|
488
|
+
throw new UsageError(
|
|
489
|
+
`${where}: --limit must be a whole number of at least 1`,
|
|
490
|
+
);
|
|
491
|
+
const t = await agentHelperTranscript(d, id, helperId, ws, limit);
|
|
492
|
+
if (p.flags["--json"]) {
|
|
493
|
+
d.out(JSON.stringify(t, null, 2));
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
d.out(
|
|
497
|
+
`${t.helper.description ?? t.helper.id} (${t.helper.status}, ${t.helper.type ?? "helper"})`,
|
|
498
|
+
);
|
|
499
|
+
if (t.omitted) d.out(`… ${t.omitted} earlier lines`);
|
|
500
|
+
for (const i of t.items) {
|
|
501
|
+
const lead =
|
|
502
|
+
i.kind === "tool"
|
|
503
|
+
? `→ ${i.tool}: `
|
|
504
|
+
: i.kind === "result"
|
|
505
|
+
? i.error
|
|
506
|
+
? " ✗ "
|
|
507
|
+
: " ← "
|
|
508
|
+
: i.kind === "prompt"
|
|
509
|
+
? "asked: "
|
|
510
|
+
: i.kind === "note"
|
|
511
|
+
? "… "
|
|
512
|
+
: "";
|
|
513
|
+
d.out(`${lead}${i.text}`);
|
|
514
|
+
}
|
|
515
|
+
d.out("");
|
|
516
|
+
d.out(t.note);
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
const r = await agentHelpers(d, id, ws);
|
|
520
|
+
const shown = p.flags["--all"]
|
|
521
|
+
? r.helpers
|
|
522
|
+
: r.helpers.filter((h) => !h.earlier);
|
|
523
|
+
if (p.flags["--json"]) {
|
|
524
|
+
d.out(
|
|
525
|
+
JSON.stringify(
|
|
526
|
+
{
|
|
527
|
+
agent: r.rec.id,
|
|
528
|
+
supported: r.supported,
|
|
529
|
+
live: r.live,
|
|
530
|
+
helpers: shown,
|
|
531
|
+
note: r.note,
|
|
532
|
+
},
|
|
533
|
+
null,
|
|
534
|
+
2,
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
if (!r.supported) {
|
|
540
|
+
d.out(`${r.rec.runtime} can't tell which helpers ${r.rec.id} started`);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
const earlier = r.helpers.length - r.helpers.filter((h) => !h.earlier).length;
|
|
544
|
+
if (!shown.length) {
|
|
545
|
+
d.out(
|
|
546
|
+
`${r.rec.id} has started no helpers${earlier && !p.flags["--all"] ? ` in this conversation (${earlier} earlier: --all)` : ""}`,
|
|
547
|
+
);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
const depth = shownDepths(shown);
|
|
551
|
+
for (const l of columns([
|
|
552
|
+
["HELPER", "WHAT", "TYPE", "MODEL", "STATE", "STARTED", "FOR", "BRANCH"],
|
|
553
|
+
...shown.map((h) => [
|
|
554
|
+
`${" ".repeat(depth.get(h.id) ?? 0)}${h.id}`,
|
|
555
|
+
(h.description ?? "-").slice(0, 48),
|
|
556
|
+
h.workflow
|
|
557
|
+
? `${h.type ?? "-"} (${h.workflow.name ?? "workflow"})`
|
|
558
|
+
: (h.type ?? "-"),
|
|
559
|
+
h.model ?? "-",
|
|
560
|
+
`${h.status}${h.earlier ? " (earlier)" : ""}`,
|
|
561
|
+
ago(h.startedAt),
|
|
562
|
+
h.durationMs === undefined ? "-" : duration(h.durationMs),
|
|
563
|
+
h.branch ?? "-",
|
|
564
|
+
]),
|
|
565
|
+
]))
|
|
566
|
+
d.out(l);
|
|
567
|
+
if (earlier && !p.flags["--all"])
|
|
568
|
+
d.out(`${earlier} more from earlier conversations: --all`);
|
|
569
|
+
d.out("");
|
|
570
|
+
d.out(r.note);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** 65 s → "1m 5s"; 2 h → "2h 0m". */
|
|
575
|
+
function duration(ms: number): string {
|
|
576
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
577
|
+
if (s < 60) return `${s}s`;
|
|
578
|
+
const m = Math.floor(s / 60);
|
|
579
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
580
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
581
|
+
}
|
|
582
|
+
|
|
459
583
|
function idAndWorkspace(
|
|
460
584
|
where: string,
|
|
461
585
|
args: string[],
|
|
@@ -962,6 +1086,8 @@ export async function cmdAgents(
|
|
|
962
1086
|
case "persona":
|
|
963
1087
|
await loadAgentWorkspaces(deps);
|
|
964
1088
|
return await persona(rest, d);
|
|
1089
|
+
case "helpers":
|
|
1090
|
+
return await helpers(rest, d);
|
|
965
1091
|
case "rename":
|
|
966
1092
|
return rename(rest, d);
|
|
967
1093
|
case "rm":
|
|
@@ -482,12 +482,21 @@ export function hubLines(
|
|
|
482
482
|
title: string,
|
|
483
483
|
): { lines: string[]; problem: boolean; check?: OnboardCheck } {
|
|
484
484
|
if (!st.configured) return { lines: [], problem: false };
|
|
485
|
-
const who = [
|
|
485
|
+
const who = [
|
|
486
|
+
st.team && `company ${st.team}`,
|
|
487
|
+
st.label && `install ${st.label}`,
|
|
488
|
+
st.role && `role ${st.role}`,
|
|
489
|
+
]
|
|
486
490
|
.filter(Boolean)
|
|
487
491
|
.join(", ");
|
|
492
|
+
// HUB-1.7: what to do about it is in Settings (Connect, Reconnect).
|
|
493
|
+
const fix =
|
|
494
|
+
st.state === "no_token" || st.state === "rejected"
|
|
495
|
+
? ` (Settings › Agents › Hub: ${st.state === "rejected" ? "Reconnect" : "Connect"})`
|
|
496
|
+
: "";
|
|
488
497
|
const detail = st.connected
|
|
489
498
|
? `${st.url}: connected${who ? ` (${who})` : ""}`
|
|
490
|
-
: `${st.url || "hub.url"}: not connected: ${st.problem ?? "no answer"}`;
|
|
499
|
+
: `${st.url || "hub.url"}: not connected: ${st.problem ?? "no answer"}${fix}`;
|
|
491
500
|
return {
|
|
492
501
|
lines: [
|
|
493
502
|
"",
|
package/src/ui/mcp/api.ts
CHANGED
|
@@ -147,8 +147,9 @@ const ACTION_ROUTES: Record<string, string> = {
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
150
|
+
* Routes with a parameter: an agent's resume (MCP agent_resume), Bring
|
|
151
|
+
* in's list and import (MCP agent_import, AG-17.1), and an agent's helpers
|
|
152
|
+
* (MCP agent_helpers, D32, GET only).
|
|
152
153
|
*/
|
|
153
154
|
const ACTION_ROUTE_PATTERNS: { re: RegExp; method: string }[] = [
|
|
154
155
|
// One held request's state (its own, the server checks). Never /decide.
|
|
@@ -161,6 +162,11 @@ const ACTION_ROUTE_PATTERNS: { re: RegExp; method: string }[] = [
|
|
|
161
162
|
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/conversations$/,
|
|
162
163
|
method: "GET",
|
|
163
164
|
},
|
|
165
|
+
// D32 (MCP agent_helpers): an agent's helpers, or one's transcript. Reads.
|
|
166
|
+
{
|
|
167
|
+
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/agents\/[A-Za-z0-9._-]{1,64}\/helpers(?:\/[A-Za-z0-9_-]{1,128})?$/,
|
|
168
|
+
method: "GET",
|
|
169
|
+
},
|
|
164
170
|
{
|
|
165
171
|
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/conversations\/[A-Za-z0-9][A-Za-z0-9_-]{0,127}\/import$/,
|
|
166
172
|
method: "POST",
|
package/src/ui/mcp/server.ts
CHANGED
|
@@ -109,13 +109,25 @@ export function createDenMcpServer(opts: DenMcpOptions): McpServer {
|
|
|
109
109
|
...(toolScope ? { scope: toolScope } : {}),
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// A pinned tool with no workspace parameter (no grants) still takes a
|
|
113
|
+
// `workspace` someone names, so that Den refuses it (callTool, scope.ts)
|
|
114
|
+
// instead of the SDK dropping the key and answering about this workspace
|
|
115
|
+
// (0.4.28-dev.0's gate: agent_helpers {workspace:"west"} answered for
|
|
116
|
+
// "default"). Not advertised; the one-shot path does the same.
|
|
117
|
+
const unpinned = new Map(toolsFor(opts.cfg).map((t) => [t.name, t]));
|
|
118
|
+
const schemaOf = (t: (typeof tools)[number]) =>
|
|
119
|
+
toolScope &&
|
|
120
|
+
!("workspace" in t.shape) &&
|
|
121
|
+
"workspace" in (unpinned.get(t.name)?.shape ?? {})
|
|
122
|
+
? z.object(t.shape).passthrough()
|
|
123
|
+
: t.shape;
|
|
112
124
|
for (const t of tools) {
|
|
113
125
|
server.registerTool(
|
|
114
126
|
t.name,
|
|
115
127
|
{
|
|
116
128
|
title: t.title,
|
|
117
129
|
description: t.description,
|
|
118
|
-
inputSchema: t
|
|
130
|
+
inputSchema: schemaOf(t),
|
|
119
131
|
annotations: {
|
|
120
132
|
readOnlyHint: !t.action,
|
|
121
133
|
destructiveHint: t.action,
|
package/src/ui/mcp/tools.ts
CHANGED
|
@@ -187,6 +187,55 @@ const read = (
|
|
|
187
187
|
): DenTool => ({ name, title, description, action: false, shape, run });
|
|
188
188
|
|
|
189
189
|
const READ_TOOLS: DenTool[] = [
|
|
190
|
+
read(
|
|
191
|
+
"agent_helpers",
|
|
192
|
+
"An agent's helpers",
|
|
193
|
+
"D32: the helpers an agent started inside its own session (Claude Code's Agent and Workflow subagents), running first: id, description, type, model, branch, status (running, idle, done, failed, stopped), statusFrom (how that was told), startedAt, lastActivityAt, durationMs, workflow, and `earlier` for those from a conversation before a relaunch. With `helper`, that one helper's transcript, tool calls summarized (`items`, the last `limit` lines). Reads only: helpers belong to the agent's session, so nothing here stops or steers one.",
|
|
194
|
+
{
|
|
195
|
+
agent: z.string().describe("The agent's id"),
|
|
196
|
+
helper: z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("A helper's id: its transcript instead of the list"),
|
|
200
|
+
limit: z
|
|
201
|
+
.number()
|
|
202
|
+
.int()
|
|
203
|
+
.min(1)
|
|
204
|
+
.max(5000)
|
|
205
|
+
.optional()
|
|
206
|
+
.describe("With helper: how many of its last lines (default 500)"),
|
|
207
|
+
workspace: workspaceArg,
|
|
208
|
+
},
|
|
209
|
+
async (a, { api, scope }) => {
|
|
210
|
+
const agent = str(a.agent);
|
|
211
|
+
if (!agent || !NAME_RE.test(agent))
|
|
212
|
+
throw new Refusal("agent_helpers: agent must be an agent id");
|
|
213
|
+
const helper = str(a.helper);
|
|
214
|
+
if (helper !== undefined && !/^[A-Za-z0-9_-]{1,128}$/.test(helper))
|
|
215
|
+
throw new Refusal("agent_helpers: helper must be a helper id");
|
|
216
|
+
let ws = str(a.workspace) ?? scope?.workspace;
|
|
217
|
+
if (!ws) {
|
|
218
|
+
const focused = (await api.get("/api/workspaces")) as {
|
|
219
|
+
focus?: unknown;
|
|
220
|
+
};
|
|
221
|
+
ws = typeof focused?.focus === "string" ? focused.focus : undefined;
|
|
222
|
+
}
|
|
223
|
+
if (!ws || !NAME_RE.test(ws))
|
|
224
|
+
throw new Refusal("agent_helpers: name the agent's workspace");
|
|
225
|
+
if (scope && ws !== scope.workspace && !(scope.grants ?? []).includes(ws))
|
|
226
|
+
throw new ScopeRefusal(
|
|
227
|
+
`agent_helpers: workspace "${ws}" is outside this agent's scope ("${scope.workspace}")`,
|
|
228
|
+
);
|
|
229
|
+
const base = `/api/workspaces/${encodeURIComponent(ws)}/agents/${encodeURIComponent(agent)}/helpers`;
|
|
230
|
+
if (!helper) return api.get(base);
|
|
231
|
+
// The limit as a query, never in the path (the client's allowlist
|
|
232
|
+
// matches the path alone).
|
|
233
|
+
return api.get(
|
|
234
|
+
`${base}/${encodeURIComponent(helper)}`,
|
|
235
|
+
typeof a.limit === "number" ? { limit: String(a.limit) } : {},
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
),
|
|
190
239
|
read(
|
|
191
240
|
"approval_check",
|
|
192
241
|
"Check an approval",
|
|
@@ -1327,6 +1376,7 @@ const WORKSPACE_TOOLS = new Set([
|
|
|
1327
1376
|
"run_tests",
|
|
1328
1377
|
"agent_resume",
|
|
1329
1378
|
"agent_import",
|
|
1379
|
+
"agent_helpers",
|
|
1330
1380
|
]);
|
|
1331
1381
|
|
|
1332
1382
|
/**
|