@netnodeag/kraftwerk 0.42.1 → 0.44.0
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/README.md +55 -6
- package/dist/acp.d.ts +131 -1
- package/dist/acp.js +163 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/harnesses/acp.js +9 -5
- package/dist/inspector/channels.d.ts +3 -0
- package/dist/inspector/channels.js +12 -1
- package/dist/inspector/chat/acp.d.ts +48 -2
- package/dist/inspector/chat/acp.js +471 -47
- package/dist/inspector/chat/backend.d.ts +30 -3
- package/dist/inspector/chat/pi.js +4 -1
- package/dist/inspector/chat/sessions.d.ts +60 -2
- package/dist/inspector/chat/sessions.js +326 -16
- package/dist/inspector/chat/store.d.ts +12 -0
- package/dist/inspector/chat/store.js +46 -0
- package/dist/inspector/chat/types.d.ts +185 -2
- package/dist/inspector/git.js +8 -5
- package/dist/inspector/instances.d.ts +5 -7
- package/dist/inspector/instances.js +14 -10
- package/dist/inspector/search.d.ts +7 -5
- package/dist/inspector/search.js +5 -2
- package/dist/inspector/server.js +117 -6
- package/inspector/dist/assets/{dist-Cn-NK0yx.js → dist-BBzscIhY.js} +1 -1
- package/inspector/dist/assets/{dist-BnxCrUQZ.js → dist-BDk9auN5.js} +1 -1
- package/inspector/dist/assets/dist-BktrTMXT.js +1 -0
- package/inspector/dist/assets/{dist-DkSZQh-S.js → dist-BprKOWfC.js} +1 -1
- package/inspector/dist/assets/{dist-BpgTFX4N.js → dist-CLig8JSu.js} +1 -1
- package/inspector/dist/assets/{dist-OsHgcj_x.js → dist-Cq-0-mqe.js} +1 -1
- package/inspector/dist/assets/{dist-z049FsVf.js → dist-DVe7MUnQ.js} +1 -1
- package/inspector/dist/assets/{dist-DQbdG8Cf.js → dist-DqfkVJaW.js} +1 -1
- package/inspector/dist/assets/{dist-C8aIUi2c.js → dist-S-LgiFkD.js} +1 -1
- package/inspector/dist/assets/{dist-cZmo-9Se.js → dist-SBBqiARl.js} +1 -1
- package/inspector/dist/assets/{dist-BJxiusVh.js → dist-wCqvkQEl.js} +1 -1
- package/inspector/dist/assets/{editor-BQkS4m10.js → editor-D__mCVTd.js} +3 -3
- package/inspector/dist/assets/index-DPdYC76U.css +1 -0
- package/inspector/dist/assets/index-De7hHAxt.js +219 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +2 -2
- package/inspector/dist/assets/dist-C6Q5FWM0.js +0 -1
- package/inspector/dist/assets/index-DGlIITXq.js +0 -218
- package/inspector/dist/assets/index-DuZ9U4BZ.css +0 -1
|
@@ -42,30 +42,213 @@ export interface ChatMeta {
|
|
|
42
42
|
scope: ChatScope;
|
|
43
43
|
/** Slug of the vibeable open in this chat's preview pane (cwd is its folder while set). */
|
|
44
44
|
vibeable?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The agent's own session id per seat ("main", or the agent slug in a
|
|
47
|
+
* channel): after a restart or the idle reaper, the next backend resumes
|
|
48
|
+
* it (session/resume) so the agent keeps its context instead of starting
|
|
49
|
+
* over with the transcript as a summary.
|
|
50
|
+
*/
|
|
51
|
+
sessions?: Record<string, string>;
|
|
45
52
|
createdAt: string;
|
|
46
53
|
updatedAt: string;
|
|
47
54
|
}
|
|
48
|
-
/**
|
|
49
|
-
export
|
|
55
|
+
/** A context compaction the agent ran, as reported on its synthetic tool call (`_meta.contextCompaction`). */
|
|
56
|
+
export interface Compaction {
|
|
57
|
+
trigger?: "manual" | "automatic";
|
|
58
|
+
preTokens?: number;
|
|
59
|
+
postTokens?: number;
|
|
60
|
+
durationMs?: number;
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A failure the agent's session ran into, as the harness reports it
|
|
65
|
+
* (AIR `sessionFailure`): what broke, how bad, and what a human can do.
|
|
66
|
+
* The same `id` with a higher `revision` replaces an earlier report.
|
|
67
|
+
*/
|
|
68
|
+
export interface SessionFailure {
|
|
69
|
+
id: string;
|
|
70
|
+
revision: number;
|
|
71
|
+
category: "connection" | "access" | "limit" | "request" | "service" | "unknown";
|
|
72
|
+
severity: "warning" | "error";
|
|
73
|
+
title: string;
|
|
74
|
+
details?: string;
|
|
75
|
+
/** Machine-readable refinement of the failure (e.g. why a login was refused). */
|
|
76
|
+
reason?: string;
|
|
77
|
+
actions: Array<"retry" | "login" | "new_session">;
|
|
78
|
+
}
|
|
79
|
+
/** One step of the agent's plan (Claude's todo list), as ACP reports it. */
|
|
80
|
+
export interface PlanEntry {
|
|
81
|
+
content: string;
|
|
82
|
+
priority: "high" | "medium" | "low";
|
|
83
|
+
status: "pending" | "in_progress" | "completed";
|
|
84
|
+
}
|
|
85
|
+
/** A slash command the agent offers (`available_commands_update`). */
|
|
86
|
+
export interface AgentCommand {
|
|
87
|
+
name: string;
|
|
88
|
+
description: string;
|
|
89
|
+
/** What the command takes after its name, if anything. */
|
|
90
|
+
hint?: string;
|
|
91
|
+
}
|
|
92
|
+
/** One session setting the agent lets the client change (`configOptions`). */
|
|
93
|
+
export type ConfigOption = {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
/** model | thought_level | model_config | mode | ... (agent-defined) */
|
|
98
|
+
category?: string;
|
|
99
|
+
} & ({
|
|
100
|
+
type: "select";
|
|
101
|
+
value: string;
|
|
102
|
+
choices: Array<{
|
|
103
|
+
value: string;
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
group?: string;
|
|
107
|
+
}>;
|
|
108
|
+
} | {
|
|
109
|
+
type: "boolean";
|
|
110
|
+
value: boolean;
|
|
111
|
+
});
|
|
112
|
+
/** Who the agent is signed in as (`_auth/status_update`). */
|
|
113
|
+
export interface AuthStatus {
|
|
114
|
+
kind: "account" | "api_key" | "gateway" | "external" | "none";
|
|
115
|
+
label: string;
|
|
116
|
+
detail?: string;
|
|
117
|
+
email?: string;
|
|
118
|
+
organization?: string;
|
|
119
|
+
plan?: string;
|
|
120
|
+
}
|
|
121
|
+
/** One field of a question the agent asks (an ACP form elicitation, e.g. Claude's AskUserQuestion). */
|
|
122
|
+
export interface ElicitationField {
|
|
123
|
+
key: string;
|
|
124
|
+
title?: string;
|
|
125
|
+
description?: string;
|
|
126
|
+
kind: "select" | "multiselect" | "text" | "number" | "boolean";
|
|
127
|
+
options?: Array<{
|
|
128
|
+
value: string;
|
|
129
|
+
label: string;
|
|
130
|
+
description?: string;
|
|
131
|
+
}>;
|
|
132
|
+
required?: boolean;
|
|
133
|
+
/** A free-text alternative to the select above it ("Other"). */
|
|
134
|
+
custom?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** A file that travels with a message (dropped or pasted into the composer); stored under the chat's folder. */
|
|
137
|
+
export interface Attachment {
|
|
138
|
+
/** File name inside the chat's attachments/ folder (unique, safe). */
|
|
139
|
+
name: string;
|
|
140
|
+
mimeType: string;
|
|
141
|
+
size: number;
|
|
142
|
+
}
|
|
143
|
+
export type ElicitationAnswer = {
|
|
144
|
+
action: "accept";
|
|
145
|
+
content: Record<string, string | number | boolean | string[]>;
|
|
146
|
+
} | {
|
|
147
|
+
action: "decline";
|
|
148
|
+
} | {
|
|
149
|
+
action: "cancel";
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* One thread event; `seq` orders them and drives SSE resume (?after=seq).
|
|
153
|
+
* `subagent` on text/thought/tool events names the child session that
|
|
154
|
+
* produced them (see the `subagent` event); absent = the main agent.
|
|
155
|
+
*/
|
|
156
|
+
export type ChatEvent =
|
|
157
|
+
/** `steered`: handed into the running turn instead of starting the next one. */
|
|
158
|
+
{
|
|
50
159
|
type: "user_message";
|
|
51
160
|
text: string;
|
|
161
|
+
steered?: boolean;
|
|
162
|
+
attachments?: Attachment[];
|
|
52
163
|
} | {
|
|
53
164
|
type: "text";
|
|
54
165
|
text: string;
|
|
166
|
+
subagent?: string;
|
|
55
167
|
} | {
|
|
56
168
|
type: "thought";
|
|
57
169
|
text: string;
|
|
170
|
+
subagent?: string;
|
|
58
171
|
} | {
|
|
59
172
|
type: "tool_call";
|
|
60
173
|
callId: string;
|
|
61
174
|
title: string;
|
|
62
175
|
kind?: string;
|
|
63
176
|
status?: string;
|
|
177
|
+
subagent?: string;
|
|
178
|
+
compaction?: Compaction;
|
|
64
179
|
} | {
|
|
65
180
|
type: "tool_update";
|
|
66
181
|
callId: string;
|
|
67
182
|
title?: string;
|
|
68
183
|
status?: string;
|
|
184
|
+
subagent?: string;
|
|
185
|
+
compaction?: Compaction;
|
|
186
|
+
}
|
|
187
|
+
/** The agent delegated to a subagent running as its own session; its events follow tagged with `subagent: sessionId`. */
|
|
188
|
+
| {
|
|
189
|
+
type: "subagent";
|
|
190
|
+
sessionId: string;
|
|
191
|
+
name: string;
|
|
192
|
+
task: string;
|
|
193
|
+
} | {
|
|
194
|
+
type: "subagent_state";
|
|
195
|
+
sessionId: string;
|
|
196
|
+
state: "completed" | "failed" | "cancelled" | "disconnected";
|
|
197
|
+
}
|
|
198
|
+
/** Background work the agent started (a backgrounded shell, a monitor, a workflow) — outlives the tool call that started it. */
|
|
199
|
+
| {
|
|
200
|
+
type: "task";
|
|
201
|
+
taskId: string;
|
|
202
|
+
name: string;
|
|
203
|
+
taskType: string;
|
|
204
|
+
description: string;
|
|
205
|
+
toolCallId?: string;
|
|
206
|
+
canStop?: boolean;
|
|
207
|
+
} | ({
|
|
208
|
+
type: "failure";
|
|
209
|
+
} & SessionFailure)
|
|
210
|
+
/** The agent's current plan; null when it dropped it. Replaces the previous one. */
|
|
211
|
+
| {
|
|
212
|
+
type: "plan";
|
|
213
|
+
entries: PlanEntry[] | null;
|
|
214
|
+
}
|
|
215
|
+
/** Context window use (tokens used of size) and the session's cost so far. */
|
|
216
|
+
| {
|
|
217
|
+
type: "usage";
|
|
218
|
+
used: number;
|
|
219
|
+
size: number;
|
|
220
|
+
costUsd?: number;
|
|
221
|
+
} | {
|
|
222
|
+
type: "commands";
|
|
223
|
+
commands: AgentCommand[];
|
|
224
|
+
} | {
|
|
225
|
+
type: "config";
|
|
226
|
+
options: ConfigOption[];
|
|
227
|
+
} | ({
|
|
228
|
+
type: "auth";
|
|
229
|
+
} & AuthStatus)
|
|
230
|
+
/** What the agent says it changed this turn (AIR file change report); `unavailable` names why there is no list. */
|
|
231
|
+
| {
|
|
232
|
+
type: "files_changed";
|
|
233
|
+
paths: string[];
|
|
234
|
+
complete: boolean;
|
|
235
|
+
uncertainty?: string;
|
|
236
|
+
unavailable?: string;
|
|
237
|
+
} | {
|
|
238
|
+
type: "elicitation_request";
|
|
239
|
+
requestId: string;
|
|
240
|
+
message: string;
|
|
241
|
+
fields: ElicitationField[];
|
|
242
|
+
} | ({
|
|
243
|
+
type: "elicitation_resolved";
|
|
244
|
+
requestId: string;
|
|
245
|
+
} & ElicitationAnswer) | {
|
|
246
|
+
type: "task_update";
|
|
247
|
+
taskId: string;
|
|
248
|
+
state?: "running" | "paused" | "completed" | "failed" | "stopped";
|
|
249
|
+
description?: string;
|
|
250
|
+
summary?: string;
|
|
251
|
+
outputFilePath?: string;
|
|
69
252
|
} | {
|
|
70
253
|
type: "permission_request";
|
|
71
254
|
requestId: string;
|
package/dist/inspector/git.js
CHANGED
|
@@ -6,8 +6,8 @@ import { ENV_FILE } from "../runner/docker.js";
|
|
|
6
6
|
import { getOutputDir, getProjectRoot } from "./context.js";
|
|
7
7
|
/**
|
|
8
8
|
* Workspace git sync. Several people run kraftwerk against one git repo, so
|
|
9
|
-
* knowledge, agents, skills and workflows travel between them like
|
|
10
|
-
* tracked file. The inspector shows what changed, lets a human pick files and
|
|
9
|
+
* knowledge, agents, skills, channels and workflows travel between them like
|
|
10
|
+
* any other tracked file. The inspector shows what changed, lets a human pick files and
|
|
11
11
|
* commit them, and pushes on request.
|
|
12
12
|
*
|
|
13
13
|
* Deliberate limits, because this runs git on someone's repo from a web UI
|
|
@@ -203,9 +203,9 @@ async function checkedOut(repo) {
|
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
205
|
* The roots that may be committed, as repo-relative paths: the workflows,
|
|
206
|
-
* knowledge, agents, skills and vibeables roots this project
|
|
207
|
-
* kraftwerk.yml. The output directory
|
|
208
|
-
* one of them.
|
|
206
|
+
* knowledge, agents, skills, channels and vibeables roots this project
|
|
207
|
+
* declares, plus kraftwerk.yml and the project README. The output directory
|
|
208
|
+
* is excluded even when it lives inside one of them.
|
|
209
209
|
*/
|
|
210
210
|
function scopeFor({ project, repoRoot }) {
|
|
211
211
|
// rev-parse reports the repo root with symlinks resolved; the project root
|
|
@@ -230,9 +230,12 @@ function scopeFor({ project, repoRoot }) {
|
|
|
230
230
|
path.resolve(project.root, project.config.knowledge ?? "knowledge"),
|
|
231
231
|
path.resolve(project.root, project.config.agents ?? "agents"),
|
|
232
232
|
path.resolve(project.root, project.config.skills ?? "skills"),
|
|
233
|
+
// Channels have no configurable root: channels/ under the project.
|
|
234
|
+
path.resolve(project.root, "channels"),
|
|
233
235
|
// Vibeables are the workspace's own apps: versioned with it, unlike clones.
|
|
234
236
|
vibeablesRootFor(project),
|
|
235
237
|
project.configPath,
|
|
238
|
+
path.resolve(project.root, "README.md"),
|
|
236
239
|
];
|
|
237
240
|
const mapped = roots.filter((r) => !!r).map(rel).filter((r) => !!r);
|
|
238
241
|
// Clones under the repos root are other people's history, never this
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentSummary } from "./agents.js";
|
|
2
|
+
import type { ChannelSummary } from "./channels.js";
|
|
2
3
|
/** Durable per-project record. The root is the key; everything else derives from it. */
|
|
3
4
|
export interface ProjectRecord {
|
|
4
5
|
root: string;
|
|
@@ -9,6 +10,8 @@ export interface ProjectRecord {
|
|
|
9
10
|
startCount: number;
|
|
10
11
|
/** Active agents, as last seen by the instance serving this root. */
|
|
11
12
|
agents?: AgentSummary[];
|
|
13
|
+
/** Channels, as last seen by the instance serving this root. */
|
|
14
|
+
channels?: ChannelSummary[];
|
|
12
15
|
}
|
|
13
16
|
/** A verified running instance, shaped like a switcher entry. */
|
|
14
17
|
export interface DiscoveredInstance {
|
|
@@ -58,13 +61,8 @@ export declare function unregisterInstance(): void;
|
|
|
58
61
|
export declare function discoverInstances(): Promise<DiscoveredInstance[]>;
|
|
59
62
|
/** Upsert the project record for a root (call on every inspector start). */
|
|
60
63
|
export declare function registerProject(root: string): Promise<void>;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* (the UI polls it, routines tick it), so a record only changes when the
|
|
64
|
-
* roster did. Roots with no record (a CLI run in a project that never
|
|
65
|
-
* started the inspector) are left alone.
|
|
66
|
-
*/
|
|
67
|
-
export declare function syncProjectAgents(root: string, agents: AgentSummary[]): Promise<void>;
|
|
64
|
+
export declare const syncProjectAgents: (root: string, agents: AgentSummary[]) => Promise<void>;
|
|
65
|
+
export declare const syncProjectChannels: (root: string, channels: ChannelSummary[]) => Promise<void>;
|
|
68
66
|
/** Stamp lastStopped on clean shutdown. Sync so exit handlers can call it. */
|
|
69
67
|
export declare function markProjectStopped(): void;
|
|
70
68
|
/** All known projects, most recently started first. */
|
|
@@ -145,6 +145,7 @@ async function readProject(file) {
|
|
|
145
145
|
lastStopped: rec.lastStopped,
|
|
146
146
|
startCount: rec.startCount ?? 1,
|
|
147
147
|
...(Array.isArray(rec.agents) ? { agents: rec.agents } : {}),
|
|
148
|
+
...(Array.isArray(rec.channels) ? { channels: rec.channels } : {}),
|
|
148
149
|
};
|
|
149
150
|
}
|
|
150
151
|
catch {
|
|
@@ -164,32 +165,35 @@ export async function registerProject(root) {
|
|
|
164
165
|
lastStarted: now,
|
|
165
166
|
startCount: (prev?.startCount ?? 0) + 1,
|
|
166
167
|
...(prev?.agents ? { agents: prev.agents } : {}),
|
|
168
|
+
...(prev?.channels ? { channels: prev.channels } : {}),
|
|
167
169
|
};
|
|
168
170
|
await fs.writeFile(projectFile(abs), JSON.stringify(rec, null, 2));
|
|
169
171
|
}
|
|
170
172
|
catch { } // best-effort, like the instance file
|
|
171
173
|
}
|
|
172
|
-
|
|
174
|
+
const synced = new Map();
|
|
173
175
|
/**
|
|
174
|
-
* Write the roster into the project's record. Called
|
|
175
|
-
* (the UI polls it, routines tick it), so a record only
|
|
176
|
-
*
|
|
177
|
-
* started the inspector) are left alone.
|
|
176
|
+
* Write the roster (or the channel list) into the project's record. Called
|
|
177
|
+
* on every read (the UI polls it, routines tick it), so a record only
|
|
178
|
+
* changes when the list did. Roots with no record (a CLI run in a project
|
|
179
|
+
* that never started the inspector) are left alone.
|
|
178
180
|
*/
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
if (key ===
|
|
181
|
+
async function syncProjectList(root, key, list) {
|
|
182
|
+
const stamp = `${root}\n${JSON.stringify(list)}`;
|
|
183
|
+
if (synced.get(key) === stamp)
|
|
182
184
|
return;
|
|
183
185
|
try {
|
|
184
186
|
const file = projectFile(root);
|
|
185
187
|
const rec = await readProject(file);
|
|
186
188
|
if (!rec)
|
|
187
189
|
return;
|
|
188
|
-
await fs.writeFile(file, JSON.stringify({ ...rec,
|
|
189
|
-
|
|
190
|
+
await fs.writeFile(file, JSON.stringify({ ...rec, [key]: list }, null, 2));
|
|
191
|
+
synced.set(key, stamp);
|
|
190
192
|
}
|
|
191
193
|
catch { }
|
|
192
194
|
}
|
|
195
|
+
export const syncProjectAgents = (root, agents) => syncProjectList(root, "agents", agents);
|
|
196
|
+
export const syncProjectChannels = (root, channels) => syncProjectList(root, "channels", channels);
|
|
193
197
|
/** Stamp lastStopped on clean shutdown. Sync so exit handlers can call it. */
|
|
194
198
|
export function markProjectStopped() {
|
|
195
199
|
if (!selfRoot)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type AgentSummary } from "./agents.js";
|
|
2
|
+
import { type ChannelSummary } from "./channels.js";
|
|
2
3
|
/**
|
|
3
|
-
* The ⌘K palette: every active agent in every workspace
|
|
4
|
-
* knows, so a user can jump to one without switching
|
|
5
|
-
* This instance reads its own
|
|
6
|
-
* project registry, where each instance records
|
|
7
|
-
*
|
|
4
|
+
* The ⌘K palette: every active agent and every channel in every workspace
|
|
5
|
+
* this machine knows, so a user can jump to one without switching
|
|
6
|
+
* workspaces first. This instance reads its own lists from disk; the others
|
|
7
|
+
* come from the project registry, where each instance records them
|
|
8
|
+
* whenever it reads them (see instances.ts). Stopped workspaces are listed too — the
|
|
8
9
|
* palette starts them on the way to the agent.
|
|
9
10
|
*/
|
|
10
11
|
export interface WorkspaceAgents {
|
|
@@ -18,6 +19,7 @@ export interface WorkspaceAgents {
|
|
|
18
19
|
current: boolean;
|
|
19
20
|
live: boolean;
|
|
20
21
|
agents: AgentSummary[];
|
|
22
|
+
channels: ChannelSummary[];
|
|
21
23
|
}
|
|
22
24
|
export interface AgentSearch {
|
|
23
25
|
workspaces: WorkspaceAgents[];
|
package/dist/inspector/search.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { resolveProject } from "../config.js";
|
|
3
3
|
import { getProjectRoot } from "./context.js";
|
|
4
4
|
import { listAgents, toSummary } from "./agents.js";
|
|
5
|
+
import { listChannels, toChannelSummary } from "./channels.js";
|
|
5
6
|
import { currentInstanceUrl, discoverWorkspaces, listProjects } from "./instances.js";
|
|
6
7
|
export async function searchAgents() {
|
|
7
8
|
const project = await resolveProject(getProjectRoot()).catch(() => null);
|
|
@@ -13,9 +14,10 @@ export async function searchAgents() {
|
|
|
13
14
|
current: true,
|
|
14
15
|
live: true,
|
|
15
16
|
agents: (await listAgents().catch(() => [])).filter((a) => !a.archived).map(toSummary),
|
|
17
|
+
channels: (await listChannels().catch(() => [])).map(toChannelSummary),
|
|
16
18
|
};
|
|
17
19
|
const [workspaces, projects] = await Promise.all([discoverWorkspaces(), listProjects()]);
|
|
18
|
-
const
|
|
20
|
+
const records = new Map(projects.map((p) => [p.root, p]));
|
|
19
21
|
const others = workspaces
|
|
20
22
|
.filter((w) => w.root && w.exists !== false)
|
|
21
23
|
.map((w) => ({
|
|
@@ -25,7 +27,8 @@ export async function searchAgents() {
|
|
|
25
27
|
root: w.root,
|
|
26
28
|
current: false,
|
|
27
29
|
live: w.live,
|
|
28
|
-
agents:
|
|
30
|
+
agents: records.get(w.root)?.agents ?? [],
|
|
31
|
+
channels: records.get(w.root)?.channels ?? [],
|
|
29
32
|
}));
|
|
30
33
|
return { workspaces: [self, ...others] };
|
|
31
34
|
}
|
package/dist/inspector/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, promises as fs } from "node:fs";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { attachmentPath, saveAttachment } from "./chat/store.js";
|
|
4
5
|
import { setOutputDir, setProjectRoot, getOutputDir, getProjectRoot } from "./context.js";
|
|
5
6
|
import { resolveProject } from "../config.js";
|
|
6
7
|
import { listRuns, getRun, readRunFile } from "./runs.js";
|
|
@@ -9,7 +10,7 @@ import { dockerStatus, triggerRun, stopRun } from "./runner.js";
|
|
|
9
10
|
import { clearNotifications, listNotifications, markNotificationsRead } from "./notifications.js";
|
|
10
11
|
import { startDiagnosis } from "./diagnose.js";
|
|
11
12
|
import { deleteChannel, getChannel, listChannels, saveChannel, channelsRoot } from "./channels.js";
|
|
12
|
-
import { cancelChat, createChat, deleteChat, disposeAllBackends, getChat, listChats, postMessage, resolvePermission, setChatVibeable, subscribeChat, convertChatToChannel, dropChannelSeats, ensureChannelChat } from "./chat/sessions.js";
|
|
13
|
+
import { cancelChat, createChat, forkChat, resetSession, answerElicitation, steerChat, stopChatTask, setChatConfig, listAgentSessionsFor, deleteChat, disposeAllBackends, getChat, listChats, postMessage, resolvePermission, setChatVibeable, subscribeChat, convertChatToChannel, dropChannelSeats, ensureChannelChat } from "./chat/sessions.js";
|
|
13
14
|
import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
|
|
14
15
|
import { deleteAgent, getAgent, listAgents, saveAgent, setAgentArchived, agentsRoot, } from "./agents.js";
|
|
15
16
|
import { deleteAgentSkill, getSkill, listAgentSkills, listSkills, readSkill, saveAgentSkill, skillsRoot, } from "./skills.js";
|
|
@@ -54,6 +55,24 @@ function json(res, body, status = 200) {
|
|
|
54
55
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
55
56
|
res.end(JSON.stringify(body));
|
|
56
57
|
}
|
|
58
|
+
/** A raw upload body (attachments), capped at `max` bytes. */
|
|
59
|
+
function readRawBody(req, max) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
let size = 0;
|
|
63
|
+
req.on("data", (c) => {
|
|
64
|
+
size += c.length;
|
|
65
|
+
if (size > max) {
|
|
66
|
+
reject(new Error(`file too large (max ${Math.round(max / 1_000_000)} MB)`));
|
|
67
|
+
req.destroy();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
chunks.push(c);
|
|
71
|
+
});
|
|
72
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
73
|
+
req.on("error", reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
57
76
|
function readBody(req) {
|
|
58
77
|
return new Promise((resolve, reject) => {
|
|
59
78
|
let data = "";
|
|
@@ -472,7 +491,7 @@ async function handleApi(req, res, url) {
|
|
|
472
491
|
return json(res, { error: err.message }, 400);
|
|
473
492
|
}
|
|
474
493
|
}
|
|
475
|
-
// GET /api/search/agents — active agents of every reachable workspace (the ⌘K palette)
|
|
494
|
+
// GET /api/search/agents — active agents and channels of every reachable workspace (the ⌘K palette)
|
|
476
495
|
if (seg.length === 3 && seg[1] === "search" && seg[2] === "agents" && method === "GET") {
|
|
477
496
|
return json(res, await searchAgents());
|
|
478
497
|
}
|
|
@@ -902,13 +921,25 @@ async function handleApi(req, res, url) {
|
|
|
902
921
|
return json(res, { error: "agent must be claude, codex, or pi" }, 400);
|
|
903
922
|
}
|
|
904
923
|
try {
|
|
905
|
-
return json(res, await createChat({ agent, scope }));
|
|
924
|
+
return json(res, await createChat({ agent, scope, ...(typeof body.resume === "string" && body.resume ? { resume: body.resume } : {}) }));
|
|
906
925
|
}
|
|
907
926
|
catch (err) {
|
|
908
927
|
return json(res, { error: err.message }, 400);
|
|
909
928
|
}
|
|
910
929
|
}
|
|
911
930
|
}
|
|
931
|
+
// GET /api/agent-sessions?agent=claude|codex — the agent's own sessions in the project root
|
|
932
|
+
if (seg.length === 2 && seg[1] === "agent-sessions" && method === "GET") {
|
|
933
|
+
const agent = url.searchParams.get("agent") ?? "claude";
|
|
934
|
+
if (!["claude", "codex", "pi"].includes(agent))
|
|
935
|
+
return json(res, { error: "agent must be claude, codex, or pi" }, 400);
|
|
936
|
+
try {
|
|
937
|
+
return json(res, { sessions: await listAgentSessionsFor(agent) });
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
return json(res, { error: err.message }, 502);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
912
943
|
// DELETE /api/chats/:id
|
|
913
944
|
if (seg.length === 3 && seg[1] === "chats" && method === "DELETE") {
|
|
914
945
|
try {
|
|
@@ -968,6 +999,60 @@ async function handleApi(req, res, url) {
|
|
|
968
999
|
return json(res, { error: err.message }, 400);
|
|
969
1000
|
}
|
|
970
1001
|
}
|
|
1002
|
+
// POST /api/chats/:id/fork — a new chat continuing a copy of this one's session
|
|
1003
|
+
if (seg.length === 4 && seg[1] === "chats" && seg[3] === "fork" && method === "POST") {
|
|
1004
|
+
try {
|
|
1005
|
+
const result = await forkChat(seg[2]);
|
|
1006
|
+
return result.error ? json(res, { error: result.error }, result.status ?? 400) : json(res, result.meta);
|
|
1007
|
+
}
|
|
1008
|
+
catch (err) {
|
|
1009
|
+
return json(res, { error: err.message }, 400);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// POST /api/chats/:id/attachments — raw file body; x-file-name names it. Returns the stored attachment.
|
|
1013
|
+
if (seg.length === 4 && seg[1] === "chats" && seg[3] === "attachments" && method === "POST") {
|
|
1014
|
+
try {
|
|
1015
|
+
if (!(await getChat(seg[2])))
|
|
1016
|
+
return json(res, { error: "not found" }, 404);
|
|
1017
|
+
const data = await readRawBody(req, 25_000_000);
|
|
1018
|
+
if (data.length === 0)
|
|
1019
|
+
return json(res, { error: "empty file" }, 400);
|
|
1020
|
+
const mimeType = (req.headers["content-type"] ?? "application/octet-stream").split(";")[0].trim();
|
|
1021
|
+
const name = await saveAttachment(seg[2], decodeURIComponent(String(req.headers["x-file-name"] ?? "file")), data);
|
|
1022
|
+
return json(res, { name, mimeType, size: data.length });
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
return json(res, { error: err.message }, 400);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// GET /api/chats/:id/attachments/:name — the stored file
|
|
1029
|
+
if (seg.length === 5 && seg[1] === "chats" && seg[3] === "attachments" && method === "GET") {
|
|
1030
|
+
try {
|
|
1031
|
+
const file = attachmentPath(seg[2], seg[4]);
|
|
1032
|
+
const ext = path.extname(file).toLowerCase();
|
|
1033
|
+
const data = await fs.readFile(file);
|
|
1034
|
+
res.writeHead(200, {
|
|
1035
|
+
"content-type": MIME[ext] ?? "application/octet-stream",
|
|
1036
|
+
"content-length": data.length,
|
|
1037
|
+
"cache-control": "private, max-age=86400",
|
|
1038
|
+
"x-content-type-options": "nosniff",
|
|
1039
|
+
});
|
|
1040
|
+
return void res.end(data);
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
return json(res, { error: "not found" }, 404);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// POST /api/chats/:id/reset-session — forget the agent's session; the next message starts fresh
|
|
1047
|
+
if (seg.length === 4 && seg[1] === "chats" && seg[3] === "reset-session" && method === "POST") {
|
|
1048
|
+
try {
|
|
1049
|
+
const result = await resetSession(seg[2]);
|
|
1050
|
+
return result.error ? json(res, { error: result.error }, result.status ?? 400) : json(res, result.meta);
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
return json(res, { error: err.message }, 400);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
971
1056
|
// POST /api/chats/:id/{message,permission,cancel}
|
|
972
1057
|
if (seg.length === 4 && seg[1] === "chats" && method === "POST") {
|
|
973
1058
|
let body = {};
|
|
@@ -981,18 +1066,44 @@ async function handleApi(req, res, url) {
|
|
|
981
1066
|
}
|
|
982
1067
|
try {
|
|
983
1068
|
let result;
|
|
1069
|
+
const attachments = Array.isArray(body.attachments)
|
|
1070
|
+
? body.attachments
|
|
1071
|
+
.filter((a) => a && typeof a.name === "string" && typeof a.mimeType === "string")
|
|
1072
|
+
.map((a) => ({ name: String(a.name), mimeType: String(a.mimeType), size: Number(a.size) || 0 }))
|
|
1073
|
+
: [];
|
|
984
1074
|
if (seg[3] === "message") {
|
|
985
1075
|
const text = String(body.text ?? "").trim();
|
|
986
|
-
if (!text)
|
|
1076
|
+
if (!text && attachments.length === 0)
|
|
987
1077
|
return json(res, { error: "text is required" }, 400);
|
|
988
1078
|
// Channels: the poster's display name signs the message.
|
|
989
|
-
result = await postMessage(seg[2], text, { from: typeof body.from === "string" ? body.from : undefined });
|
|
1079
|
+
result = await postMessage(seg[2], text || "(see attached)", { from: typeof body.from === "string" ? body.from : undefined, attachments });
|
|
990
1080
|
}
|
|
991
1081
|
else if (seg[3] === "permission") {
|
|
992
1082
|
result = await resolvePermission(seg[2], String(body.requestId ?? ""), body.optionId == null ? null : String(body.optionId));
|
|
993
1083
|
}
|
|
994
1084
|
else if (seg[3] === "cancel") {
|
|
995
|
-
|
|
1085
|
+
// Channels: `agent` stops one member; without it every agent in the chat.
|
|
1086
|
+
result = await cancelChat(seg[2], typeof body.agent === "string" && body.agent ? body.agent : undefined);
|
|
1087
|
+
}
|
|
1088
|
+
else if (seg[3] === "steer") {
|
|
1089
|
+
const text = String(body.text ?? "").trim();
|
|
1090
|
+
if (!text && attachments.length === 0)
|
|
1091
|
+
return json(res, { error: "text is required" }, 400);
|
|
1092
|
+
result = await steerChat(seg[2], text || "(see attached)", attachments);
|
|
1093
|
+
}
|
|
1094
|
+
else if (seg[3] === "elicitation") {
|
|
1095
|
+
const action = body.action === "accept" || body.action === "decline" || body.action === "cancel" ? body.action : null;
|
|
1096
|
+
if (!action)
|
|
1097
|
+
return json(res, { error: "action must be accept, decline, or cancel" }, 400);
|
|
1098
|
+
const content = body.content && typeof body.content === "object" ? body.content : {};
|
|
1099
|
+
result = await answerElicitation(seg[2], String(body.requestId ?? ""), action === "accept" ? { action, content } : { action });
|
|
1100
|
+
}
|
|
1101
|
+
else if (seg[3] === "task-stop") {
|
|
1102
|
+
result = await stopChatTask(seg[2], String(body.taskId ?? ""));
|
|
1103
|
+
}
|
|
1104
|
+
else if (seg[3] === "config") {
|
|
1105
|
+
const value = typeof body.value === "boolean" ? body.value : String(body.value ?? "");
|
|
1106
|
+
result = await setChatConfig(seg[2], String(body.configId ?? ""), value);
|
|
996
1107
|
}
|
|
997
1108
|
else {
|
|
998
1109
|
return json(res, { error: "not found" }, 404);
|