dsh-taskboard 0.5.4 → 0.6.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 +27 -160
- package/lib/client.js +2564 -678
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +31 -1
- package/lib/host/routes.js.map +1 -1
- package/lib/host/session-sync.js +449 -0
- package/lib/host/session-sync.js.map +1 -0
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +109 -2
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +27 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +75 -75
- package/src/client/api.ts +8 -0
- package/src/client/board/AlertModal.tsx +3 -1
- package/src/client/board/ImportModal.tsx +26 -24
- package/src/client/board/SettingsModal.tsx +98 -22
- package/src/client/board/SlashPromptInput.tsx +272 -0
- package/src/client/board/TaskBoard.tsx +53 -49
- package/src/client/board/TaskCard.tsx +33 -21
- package/src/client/board/TaskDetail.tsx +169 -104
- package/src/client/board/TaskFormModal.tsx +254 -202
- package/src/client/board/TemplateManager.tsx +32 -29
- package/src/client/board/labels.ts +36 -27
- package/src/client/controller.ts +62 -2
- package/src/client/i18n/en.ts +455 -0
- package/src/client/i18n/runtime.ts +155 -0
- package/src/client/i18n/zh.ts +460 -0
- package/src/client/index.ts +182 -42
- package/src/client/sidebar-entry.ts +13 -3
- package/src/client/styles.ts +131 -0
- package/src/host/execution.ts +14 -1
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +650 -0
- package/src/host/store.ts +15 -1
- package/src/index.ts +125 -1
- package/src/shared/api.ts +49 -0
- package/src/shared/protocol.ts +54 -0
- package/src/shared/version.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerTaskboardRoutes } from "./host/routes.js";
|
|
|
7
7
|
import { SchedulerService } from "./host/scheduler.js";
|
|
8
8
|
import { TaskStore } from "./host/store.js";
|
|
9
9
|
import { TemplateStore } from "./host/templates.js";
|
|
10
|
+
import { ExternalSessionSyncService } from "./host/session-sync.js";
|
|
10
11
|
//#region src/index.ts
|
|
11
12
|
/** Ledger file name under the DSH home. */
|
|
12
13
|
const LEDGER_FILE = "dsh-taskboard.json";
|
|
@@ -49,10 +50,35 @@ function apply(ctx) {
|
|
|
49
50
|
modelProviders
|
|
50
51
|
}));
|
|
51
52
|
const events = { onSessionEvent: (listener) => wsCtx.on("session/event", (session, event) => {
|
|
52
|
-
listener(session.id, event);
|
|
53
|
+
listener(session.id, event, session);
|
|
53
54
|
}) };
|
|
55
|
+
let agentSessions;
|
|
56
|
+
const sessionSync = new ExternalSessionSyncService({
|
|
57
|
+
store,
|
|
58
|
+
workspaces: workspaceFace(wsCtx.workspaceRegistry),
|
|
59
|
+
events,
|
|
60
|
+
sessions: {
|
|
61
|
+
get: (id) => {
|
|
62
|
+
try {
|
|
63
|
+
return (agentSessions ?? wsCtx.get("sessions") ?? wsCtx.get("sessionRegistry") ?? wsCtx.root?.get("sessions"))?.get?.(id);
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
list: () => {
|
|
69
|
+
try {
|
|
70
|
+
return (agentSessions ?? wsCtx.get("sessions") ?? wsCtx.get("sessionRegistry") ?? wsCtx.root?.get("sessions"))?.list?.() ?? [];
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
now
|
|
77
|
+
});
|
|
78
|
+
disposers.push(() => sessionSync.dispose());
|
|
54
79
|
const git = createGitFace();
|
|
55
80
|
wsCtx.inject(["agents"], (agentCtx) => {
|
|
81
|
+
agentSessions = agentCtx.get("sessions");
|
|
56
82
|
const execution = new ExecutionService({
|
|
57
83
|
store,
|
|
58
84
|
agents: { create: (options) => agentCtx.agents.create(options) },
|
|
@@ -94,6 +120,13 @@ function apply(ctx) {
|
|
|
94
120
|
return;
|
|
95
121
|
}
|
|
96
122
|
},
|
|
123
|
+
setPermission: (sessionId, permission) => {
|
|
124
|
+
try {
|
|
125
|
+
const permService = agentCtx.get("permissionPresets");
|
|
126
|
+
const session = agentCtx.get("sessions")?.get(sessionId);
|
|
127
|
+
if (session !== void 0 && permService !== void 0) permService.set(session, permission);
|
|
128
|
+
} catch {}
|
|
129
|
+
},
|
|
97
130
|
maxConcurrent
|
|
98
131
|
});
|
|
99
132
|
let disposeRoutes;
|
|
@@ -106,7 +139,81 @@ function apply(ctx) {
|
|
|
106
139
|
cancel: (taskId) => execution.cancel(taskId),
|
|
107
140
|
modelProviders,
|
|
108
141
|
git,
|
|
109
|
-
templates
|
|
142
|
+
templates,
|
|
143
|
+
promptCompletions: async () => {
|
|
144
|
+
try {
|
|
145
|
+
const skillsService = agentCtx.get("skills");
|
|
146
|
+
const commandsService = agentCtx.get("commands");
|
|
147
|
+
const rawSkills = skillsService?.list ? await skillsService.list().catch(() => []) : [];
|
|
148
|
+
const rawCommands = commandsService?.list ? commandsService.list() : [];
|
|
149
|
+
return {
|
|
150
|
+
skills: Array.isArray(rawSkills) ? rawSkills.map((s) => ({
|
|
151
|
+
name: s.name,
|
|
152
|
+
description: s.description
|
|
153
|
+
})) : [],
|
|
154
|
+
commands: Array.isArray(rawCommands) ? rawCommands.map((c) => ({
|
|
155
|
+
name: c.name,
|
|
156
|
+
description: c.description,
|
|
157
|
+
hint: c.input?.hint
|
|
158
|
+
})) : []
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return {
|
|
162
|
+
skills: [],
|
|
163
|
+
commands: []
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
modelCatalog: async () => {
|
|
168
|
+
try {
|
|
169
|
+
const models = [];
|
|
170
|
+
const llm = agentCtx.get("llm") ?? wsCtx.get("llm");
|
|
171
|
+
if (llm?.listProviders !== void 0 && llm.listModels !== void 0) {
|
|
172
|
+
const providers = llm.listProviders();
|
|
173
|
+
for (const p of providers) try {
|
|
174
|
+
const list = await llm.listModels(p.id);
|
|
175
|
+
for (const m of list) {
|
|
176
|
+
let reasoning;
|
|
177
|
+
try {
|
|
178
|
+
const meta = llm.resolveModelInfo !== void 0 ? await llm.resolveModelInfo(p.id, m.id) : llm.resolveModel !== void 0 ? await llm.resolveModel(p.id, m.id) : void 0;
|
|
179
|
+
if (meta?.reasoning !== void 0) reasoning = meta.reasoning;
|
|
180
|
+
} catch {}
|
|
181
|
+
models.push({
|
|
182
|
+
provider: p.id,
|
|
183
|
+
model: m.id,
|
|
184
|
+
name: m.name,
|
|
185
|
+
...m.description ? { description: m.description } : {},
|
|
186
|
+
...reasoning !== void 0 ? { reasoning } : {}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
const presetsService = agentCtx.get("agentPresets");
|
|
192
|
+
const presets = [];
|
|
193
|
+
let defaultPresetId;
|
|
194
|
+
if (presetsService?.list !== void 0) try {
|
|
195
|
+
const raw = await presetsService.list();
|
|
196
|
+
const list = raw.ok === true ? raw.value.presets : Array.isArray(raw) ? raw : [];
|
|
197
|
+
for (const p of list) {
|
|
198
|
+
presets.push({
|
|
199
|
+
id: p.id,
|
|
200
|
+
name: p.name
|
|
201
|
+
});
|
|
202
|
+
if (p.isDefault) defaultPresetId = p.id;
|
|
203
|
+
}
|
|
204
|
+
} catch {}
|
|
205
|
+
return {
|
|
206
|
+
models,
|
|
207
|
+
presets,
|
|
208
|
+
...defaultPresetId !== void 0 ? { defaultPresetId } : {}
|
|
209
|
+
};
|
|
210
|
+
} catch {
|
|
211
|
+
return {
|
|
212
|
+
models: [],
|
|
213
|
+
presets: []
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
110
217
|
});
|
|
111
218
|
return () => disposeRoutes?.();
|
|
112
219
|
});
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the ten\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { DEFAULT_MAX_CONCURRENT, ExecutionService, type EventsFace } from './host/execution.ts'\nimport { createGitFace } from './host/git.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { TemplateStore } from './host/templates.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Task-template side file name under the DSH home (0.4.0). */\nexport const TEMPLATES_FILE = 'dsh-taskboard-templates.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const templates = new TemplateStore(dshHomePath(TEMPLATES_FILE))\n // Eager first load: the tools and most routes read snapshot()/get() without\n // triggering the lazy load, so a fresh boot used to serve an EMPTY board to\n // taskboard_list/get until the scheduler catchup tick or the first\n // GET /state happened to load the file (review P0). load() never throws —\n // a corrupt ledger is quarantined instead.\n void store.load()\n const now = () => Date.now()\n // Global execution concurrency cap (DSH_TASKBOARD_MAX_CONCURRENT overrides).\n const maxConcurrent = Math.max(1, Number.parseInt(process.env.DSH_TASKBOARD_MAX_CONCURRENT ?? '', 10) || DEFAULT_MAX_CONCURRENT)\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n\n // Registered model provider routes (from the host llm runtime), read\n // lazily at call time so late availability still applies; undefined when\n // the runtime is absent → only structural model validation runs.\n const modelProviders = (): string[] | undefined => {\n try {\n const llm = wsCtx.get('llm') as { listProviders?: () => Array<{ id: string }> } | undefined\n return llm === undefined || typeof llm.listProviders !== 'function'\n ? undefined\n : llm.listProviders().map(p => p.id)\n } catch { return undefined }\n }\n\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n modelProviders,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown })\n }),\n }\n\n // The narrow git face shared by execution (worktree isolation) and the\n // routes (merge / remove / workspace detection).\n const git = createGitFace()\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n git,\n // Preset composition (0.3.3): mirror apiproxy's composeAgent — resolve\n // the id BEFORE creation (the session header snapshots meta), mount\n // inside the factory's setup callback. No roster service → undefined\n // (bare host composition, the pre-preset behavior).\n composeAgent: async (presetId) => {\n const presets = agentCtx.get('agentPresets') as {\n resolve(id?: string): Promise<{ id: string }>\n mount(agentCtx: unknown, id?: string): Promise<unknown>\n } | undefined\n if (presets === undefined) return undefined\n const resolved = await presets.resolve(presetId)\n return {\n agentPreset: resolved.id,\n setup: async (ctx: unknown) => { await presets.mount(ctx, resolved.id) },\n }\n },\n renameSession: (sessionId, title) => {\n // Best-effort: pin the execution session's title to the task title\n // through the log-backed session-title service (user-sourced rename).\n try {\n const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined\n const sessionTitle = agentCtx.get('sessionTitle') as { rename(session: unknown, title: string): unknown } | undefined\n const session = sessions?.get(sessionId)\n if (session !== undefined && sessionTitle !== undefined) sessionTitle.rename(session, title)\n } catch { /* cosmetic */ }\n },\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n maxConcurrent,\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string, runOptions?: { reuseWorktree?: boolean }) => execution.run(taskId, 'manual', runOptions),\n cancel: (taskId: string) => execution.cancel(taskId),\n modelProviders,\n git,\n templates,\n })\n return () => disposeRoutes?.()\n })\n\n // Startup reconciliation: executions left 'running' by a previous host\n // process are marked failed and their tasks handed back to todo (their\n // settlement watchers died with that process).\n void execution.reconcile()\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open. Shares the execution concurrency cap.\n const scheduler = new SchedulerService({ store, execution, now, maxConcurrent })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n // Detach the settlement listener with the plugin — a hot reload must\n // not leave stale services reacting to turn/end errors (review P1).\n disposers.push(() => execution.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":";;;;;;;;;;;AAgCA,MAAa,cAAc;;AAG3B,MAAa,iBAAiB;;AAG9B,MAAa,OAAO;;AAGpB,MAAa,SAAS,CAAC,SAAS,cAAc;;;;;AAM9C,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,IAAI,UAAU,EAAE,MAAM,YAAY,WAAW,EAAE,CAAC;CAC9D,MAAM,YAAY,IAAI,cAAc,YAAY,cAAc,CAAC;CAM/D,MAAW,KAAK;CAChB,MAAM,YAAY,KAAK,IAAI;CAE3B,MAAM,gBAAgB,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,gCAAgC,IAAI,EAAE,KAAA,CAA2B;CAG/H,MAAM,iBAAiB,IAAI,aAAa,QAAQ;EAC9C,MAAM;EACN,OAAA;EACA,MAAM;CACR,CAAC;CACD,IAAI,aAAa,gBAAgB,iCAAiC;CAIlE,IAAI,OAAO,CAAC,mBAAmB,IAAI,UAAmB;EACpD,MAAM,YAA+B,CAAC;EAKtC,MAAM,uBAA6C;GACjD,IAAI;IACF,MAAM,MAAM,MAAM,IAAI,KAAK;IAC3B,OAAO,QAAQ,KAAA,KAAa,OAAO,IAAI,kBAAkB,aACrD,KAAA,IACA,IAAI,cAAc,CAAC,CAAC,KAAI,MAAK,EAAE,EAAE;GACvC,QAAQ;IAAE;GAAiB;EAC7B;EAEA,UAAU,KAAK,GAAG,uBAAuB,OAAO;GAC9C;GACA,YAAY,cAAc,MAAM,iBAAiB;GACjD;GACA;EACF,CAAC,CAAC;EAGF,MAAM,SAAqB,EACzB,iBAAiB,aAAa,MAAM,GAAG,kBAAkB,SAAS,UAAU;GAC1E,SAAS,QAAQ,IAAI,KAAyC;EAChE,CAAC,EACH;EAIA,MAAM,MAAM,cAAc;EAE1B,MAAM,OAAO,CAAC,QAAQ,IAAI,aAAsB;GAC9C,MAAM,YAAY,IAAI,iBAAiB;IACrC;IACA,QAAQ,EACN,SAAS,YAA4B,SAAS,OAAO,OAAO,OAAgB,EAC9E;IACA,YAAY;KACV,MAAK,OAAM,cAAc,MAAM,iBAAiB,CAAC,CAAC,IAAI,EAAE;KACxD,QAAQ,OAAO,aAAa,cAAc;MACxC,MAAM,KAAK,MAAM,kBAAkB,IAAI,WAAoB;MAC3D,IAAI,OAAO,KAAA,GAAW,MAAM,GAAG,cAAc,SAAkB;KACjE;IACF;IACA;IACA;IACA;IAKA,cAAc,OAAO,aAAa;KAChC,MAAM,UAAU,SAAS,IAAI,cAAc;KAI3C,IAAI,YAAY,KAAA,GAAW,OAAO,KAAA;KAClC,MAAM,WAAW,MAAM,QAAQ,QAAQ,QAAQ;KAC/C,OAAO;MACL,aAAa,SAAS;MACtB,OAAO,OAAO,QAAiB;OAAE,MAAM,QAAQ,MAAM,KAAK,SAAS,EAAE;MAAE;KACzE;IACF;IACA,gBAAgB,WAAW,UAAU;KAGnC,IAAI;MACF,MAAM,WAAW,SAAS,IAAI,UAAU;MACxC,MAAM,eAAe,SAAS,IAAI,cAAc;MAChD,MAAM,UAAU,UAAU,IAAI,SAAS;MACvC,IAAI,YAAY,KAAA,KAAa,iBAAiB,KAAA,GAAW,aAAa,OAAO,SAAS,KAAK;KAC7F,QAAQ,CAAiB;IAC3B;IACA,oBAAoB;KAClB,IAAI;MACF,MAAM,YAAY,SAAS,IAAI,mBAAmB;MAClD,MAAM,OAAO,WAAW;MACxB,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK,KAAK,SAAS;KAC7D,QAAQ;MAAE;KAAiB;IAC7B;IACA;GACF,CAAC;GAGD,IAAI;GACJ,SAAS,OAAO,CAAC,WAAW,IAAI,WAAoB;IAClD,gBAAgB,wBAAwB,QAAQ;KAC9C;KACA,YAAY,cAAc,MAAM,iBAAiB;KACjD;KACA,MAAM,QAAgB,eAA6C,UAAU,IAAI,QAAQ,UAAU,UAAU;KAC7G,SAAS,WAAmB,UAAU,OAAO,MAAM;KACnD;KACA;KACA;IACF,CAAC;IACD,aAAa,gBAAgB;GAC/B,CAAC;GAKD,UAAe,UAAU;GAIzB,MAAM,YAAY,IAAI,iBAAiB;IAAE;IAAO;IAAW;IAAK;GAAc,CAAC;GAC/E,UAAU,MAAM;GAChB,UAAU,WAAW,UAAU,QAAQ,CAAC;GAGxC,UAAU,WAAW,UAAU,QAAQ,CAAC;GAExC,aAAa;IACX,gBAAgB;IAChB,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACrD;EACF,CAAC;EAED,aAAa;GACX,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;EACrD;CACF,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the ten\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { DEFAULT_MAX_CONCURRENT, ExecutionService, type EventsFace } from './host/execution.ts'\nimport { createGitFace } from './host/git.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { TemplateStore } from './host/templates.ts'\nimport { ExternalSessionSyncService } from './host/session-sync.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Task-template side file name under the DSH home (0.4.0). */\nexport const TEMPLATES_FILE = 'dsh-taskboard-templates.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const templates = new TemplateStore(dshHomePath(TEMPLATES_FILE))\n // Eager first load: the tools and most routes read snapshot()/get() without\n // triggering the lazy load, so a fresh boot used to serve an EMPTY board to\n // taskboard_list/get until the scheduler catchup tick or the first\n // GET /state happened to load the file (review P0). load() never throws —\n // a corrupt ledger is quarantined instead.\n void store.load()\n const now = () => Date.now()\n // Global execution concurrency cap (DSH_TASKBOARD_MAX_CONCURRENT overrides).\n const maxConcurrent = Math.max(1, Number.parseInt(process.env.DSH_TASKBOARD_MAX_CONCURRENT ?? '', 10) || DEFAULT_MAX_CONCURRENT)\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n\n // Registered model provider routes (from the host llm runtime), read\n // lazily at call time so late availability still applies; undefined when\n // the runtime is absent → only structural model validation runs.\n const modelProviders = (): string[] | undefined => {\n try {\n const llm = wsCtx.get('llm') as { listProviders?: () => Array<{ id: string }> } | undefined\n return llm === undefined || typeof llm.listProviders !== 'function'\n ? undefined\n : llm.listProviders().map(p => p.id)\n } catch { return undefined }\n }\n\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n modelProviders,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown }, session as never)\n }),\n }\n\n let agentSessions: { get?: (id: string) => unknown; list?: () => unknown[] } | undefined\n\n // External workspace sessions sync service (0.5.4).\n const sessionSync = new ExternalSessionSyncService({\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n events,\n sessions: {\n get: id => {\n try {\n const registry = (agentSessions ?? wsCtx.get('sessions') ?? wsCtx.get('sessionRegistry') ?? wsCtx.root?.get('sessions')) as { get?: (id: string) => unknown } | undefined\n return registry?.get?.(id)\n } catch { return undefined }\n },\n list: () => {\n try {\n const registry = (agentSessions ?? wsCtx.get('sessions') ?? wsCtx.get('sessionRegistry') ?? wsCtx.root?.get('sessions')) as { list?: () => unknown[] } | undefined\n return registry?.list?.() ?? []\n } catch { return [] }\n },\n },\n now,\n })\n disposers.push(() => sessionSync.dispose())\n\n // The narrow git face shared by execution (worktree isolation) and the\n // routes (merge / remove / workspace detection).\n const git = createGitFace()\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n agentSessions = agentCtx.get('sessions') as { get?: (id: string) => unknown; list?: () => unknown[] } | undefined\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n git,\n // Preset composition (0.3.3): mirror apiproxy's composeAgent — resolve\n // the id BEFORE creation (the session header snapshots meta), mount\n // inside the factory's setup callback. No roster service → undefined\n // (bare host composition, the pre-preset behavior).\n composeAgent: async (presetId) => {\n const presets = agentCtx.get('agentPresets') as {\n resolve(id?: string): Promise<{ id: string }>\n mount(agentCtx: unknown, id?: string): Promise<unknown>\n } | undefined\n if (presets === undefined) return undefined\n const resolved = await presets.resolve(presetId)\n return {\n agentPreset: resolved.id,\n setup: async (ctx: unknown) => { await presets.mount(ctx, resolved.id) },\n }\n },\n renameSession: (sessionId, title) => {\n // Best-effort: pin the execution session's title to the task title\n // through the log-backed session-title service (user-sourced rename).\n try {\n const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined\n const sessionTitle = agentCtx.get('sessionTitle') as { rename(session: unknown, title: string): unknown } | undefined\n const session = sessions?.get(sessionId)\n if (session !== undefined && sessionTitle !== undefined) sessionTitle.rename(session, title)\n } catch { /* cosmetic */ }\n },\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n setPermission: (sessionId, permission) => {\n try {\n const permService = agentCtx.get('permissionPresets') as { set(session: unknown, name: string): void } | undefined\n const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined\n const session = sessions?.get(sessionId)\n if (session !== undefined && permService !== undefined) {\n permService.set(session, permission)\n }\n } catch { /* cosmetic */ }\n },\n maxConcurrent,\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string, runOptions?: { reuseWorktree?: boolean }) => execution.run(taskId, 'manual', runOptions),\n cancel: (taskId: string) => execution.cancel(taskId),\n modelProviders,\n git,\n templates,\n promptCompletions: async () => {\n try {\n const skillsService = agentCtx.get('skills') as { list?(options?: unknown): Promise<Array<{ name: string; description?: string }>> } | undefined\n const commandsService = agentCtx.get('commands') as { list?(): Array<{ name: string; description?: string; input?: { hint?: string } }> } | undefined\n const rawSkills = skillsService?.list ? await skillsService.list().catch(() => []) : []\n const rawCommands = commandsService?.list ? commandsService.list() : []\n return {\n skills: Array.isArray(rawSkills) ? rawSkills.map(s => ({ name: s.name, description: s.description })) : [],\n commands: Array.isArray(rawCommands) ? rawCommands.map(c => ({ name: c.name, description: c.description, hint: c.input?.hint })) : [],\n }\n } catch {\n return { skills: [], commands: [] }\n }\n },\n modelCatalog: async () => {\n try {\n type ModelItem = {\n provider: string\n model: string\n name?: string\n description?: string\n reasoning?: {\n efforts: Array<{ id: string; name: string; description?: string }>\n defaultEffort?: string\n }\n }\n const models: ModelItem[] = []\n\n const llm = (agentCtx.get('llm') ?? wsCtx.get('llm')) as {\n listProviders?(): Array<{ id: string; name?: string }>\n listModels?(provider: string): Promise<Array<{ id: string; name?: string; description?: string }>>\n resolveModelInfo?(provider: string, model: string): Promise<{ reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }>\n resolveModel?(provider: string, model: string): Promise<{ reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }>\n } | undefined\n\n if (llm?.listProviders !== undefined && llm.listModels !== undefined) {\n const providers = llm.listProviders()\n for (const p of providers) {\n try {\n const list = await llm.listModels(p.id)\n for (const m of list) {\n let reasoning: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } | undefined\n try {\n const meta = llm.resolveModelInfo !== undefined\n ? await llm.resolveModelInfo(p.id, m.id)\n : llm.resolveModel !== undefined ? await llm.resolveModel(p.id, m.id) : undefined\n if (meta?.reasoning !== undefined) {\n reasoning = meta.reasoning\n }\n } catch { /* ignore */ }\n\n models.push({\n provider: p.id,\n model: m.id,\n name: m.name,\n ...(m.description ? { description: m.description } : {}),\n ...(reasoning !== undefined ? { reasoning } : {}),\n })\n }\n } catch { /* continue */ }\n }\n }\n\n const presetsService = agentCtx.get('agentPresets') as {\n list?(): Promise<{ ok: boolean; value?: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } } | Array<{ id: string; name?: string; isDefault?: boolean }>>\n } | undefined\n const presets: Array<{ id: string; name?: string }> = []\n let defaultPresetId: string | undefined\n\n if (presetsService?.list !== undefined) {\n try {\n const raw = await presetsService.list()\n const list = (raw as { ok?: boolean; value?: { presets?: unknown[] } }).ok === true\n ? (raw as { value: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } }).value.presets\n : Array.isArray(raw) ? raw : []\n for (const p of list) {\n presets.push({ id: p.id, name: p.name })\n if (p.isDefault) defaultPresetId = p.id\n }\n } catch { /* continue */ }\n }\n\n return { models, presets, ...(defaultPresetId !== undefined ? { defaultPresetId } : {}) }\n } catch {\n return { models: [], presets: [] }\n }\n },\n })\n return () => disposeRoutes?.()\n })\n\n // Startup reconciliation: executions left 'running' by a previous host\n // process are marked failed and their tasks handed back to todo (their\n // settlement watchers died with that process).\n void execution.reconcile()\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open. Shares the execution concurrency cap.\n const scheduler = new SchedulerService({ store, execution, now, maxConcurrent })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n // Detach the settlement listener with the plugin — a hot reload must\n // not leave stale services reacting to turn/end errors (review P1).\n disposers.push(() => execution.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":"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"}
|
package/lib/shared/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","names":[],"sources":["../../src/shared/api.ts"],"sourcesContent":["/**\n * Wire contract for the /taskboard host routes: the JSON envelope,\n * request/response shapes, and SSE event payloads shared by the host routes\n * and the browser client.\n *\n * @module dsh-taskboard/shared/api\n */\nimport type { BoardSettings, TaskLedger, TaskModel, TaskRecord, TaskSummary } from './protocol.ts'\n\nexport type { TaskModel, TaskRecord }\n\n/** Route prefix on the shared DSH webserver (same origin as the GUI). */\nexport const ROUTE_PREFIX = '/dsh-taskboard'\n\n/** SSE stream path (exact route; longest-prefix wins keep it disjoint). */\nexport const SSE_PATH = '/dsh-taskboard/events'\n\n/** Stable error codes (mirror the tool-level codes plus HTTP mapping). */\nexport type ApiErrorCode =\n | 'invalid_input'\n | 'not_found'\n | 'version_conflict'\n | 'invalid_transition'\n | 'forbidden'\n | 'internal'\n\n/** Success envelope. */\nexport type ApiOk<T> = { ok: true; value: T }\n\n/** Failure envelope. */\nexport type ApiFail = { ok: false; error: { code: ApiErrorCode; message: string } }\n\n/** The envelope either way. */\nexport type ApiResult<T> = ApiOk<T> | ApiFail\n\n// ---------------------------------------------------------------------------\n// payloads\n// ---------------------------------------------------------------------------\n\n/** Full-state response (the reconnect baseline after an SSE gap). */\nexport type StateResponse = TaskLedger\n\n/** Workspace listing for the UI pickers. */\nexport type WorkspaceView = { id: string; path: string; title: string; sessionCount: number; gitAvailable?: boolean }\n\n/** Create-task request body (actor is always the GUI user). */\nexport type CreateTaskBody = {\n title: string\n workspaceId: string\n urgency: string\n description?: string\n prompt?: string\n execution?: { mode?: string; cron?: string }\n model?: TaskModel\n /** Code isolation for executions ('worktree' | 'none'); omitted = default. */\n isolation?: string\n /** Agent preset for execution sessions; omitted = deployment default. */\n presetId?: string\n /** Acceptance checklist item texts (host mints ids, all unchecked). */\n checklist?: string[]\n}\n\n/** Update-task request body (ifVersion mandatory). */\nexport type UpdateTaskBody = {\n ifVersion: number\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n blocked?: boolean\n /** Rebind the task to another project (GUI owner surface only). */\n workspaceId?: string\n execution?: { mode?: string; cron?: string }\n model?: TaskModel | null\n /** Change isolation; locked once the task has execution history. */\n isolation?: string\n /** Change the execution preset (takes effect on the next run). */\n presetId?: string | null\n /** Replace the whole checklist (GUI owner surface); null clears it. */\n checklist?: unknown\n}\n\n/** Move-task request body (ifVersion mandatory; the user MAY move to done). */\nexport type MoveTaskBody = { ifVersion: number; status: string }\n\n/**\n * Quick-reject request body (card ✗ button): move back to todo plus an\n * optional user comment, committed as ONE ledger mutation so a failed move\n * can never strand an orphan comment.\n */\nexport type RejectTaskBody = { ifVersion: number; body?: string }\n\n/** Comment request body. */\nexport type CommentBody = { body: string }\n\n/** Delete request body (purge=true physically removes a trashed task). */\nexport type DeleteTaskBody = { ifVersion?: number; purge?: boolean }\n\n/** Run request body; `reuse: true` = 续跑 (keep a live worktree as-is). */\nexport type RunTaskBody = { reuse?: boolean }\n\n/** Merge outcome: `noop: true` = the branch had no commits over HEAD (nothing merged). */\nexport type MergeBranchResponse = { merged: boolean; noop?: boolean; branch: string }\n\n/** Remove a task's worktree; optionally delete its branch too. */\nexport type WorktreeRemoveBody = { deleteBranch?: boolean }\n\n/** One orphan worktree directory (exists on disk, owned by no live task). */\nexport type OrphanWorktree = { workspaceId: string; workspacePath: string; taskId: string; path: string }\n\n/** A git-enabled workspace whose .gitignore does not cover the worktree dir. */\nexport type GitignoreSuggestion = { workspaceId: string; workspacePath: string }\n\n/** Health-diagnostics response (⚙ panel). */\nexport type DiagnosticsResponse = {\n revision: number\n tasks: number\n /** Executions currently marked `running`. */\n staleRunning: number\n /** Worktree directories whose task no longer exists in the ledger. */\n orphanWorktrees: OrphanWorktree[]\n /** Git workspaces whose .gitignore does not ignore the worktree dir. */\n gitIgnoreSuggestions: GitignoreSuggestion[]\n}\n\n/** Fields a task template may prefill (0.4.0). */\nexport type TaskTemplateSpec = {\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n execution?: { mode?: string; cron?: string }\n model?: TaskModel\n isolation?: string\n presetId?: string\n /** Checklist item texts (host mints ids at create time). */\n checklist?: string[]\n}\n\n/** One reusable task template (0.4.0). */\nexport type TaskTemplate = {\n id: string\n name: string\n task: TaskTemplateSpec\n /** Seeded built-in templates (kept on load, deletable like any other). */\n builtin?: boolean\n createdAt: number\n updatedAt: number\n}\n\n/** Templates listing response. */\nexport type TemplatesResponse = { templates: TaskTemplate[] }\n\n/** Board-settings response (0.5.0; absent fields follow factory defaults). */\nexport type SettingsResponse = BoardSettings\n\n/** Update-board-settings request body (0.5.0; whole-object replace semantics). */\nexport type UpdateSettingsBody = {\n /** Default code isolation for NEW tasks ('worktree' | 'none'). */\n defaultIsolation?: string\n}\n\n/** Import dry-run response (0.4.0): every task classified, nothing written. */\nexport type ImportPreviewResponse = {\n plan: {\n create: Array<{ id: string; title: string; status: string }>\n overwrite: Array<{ id: string; title: string; status: string }>\n invalid: Array<{ id?: string; reason: string }>\n }\n}\n\n/** Import commit response. */\nexport type ImportCommitResponse = {\n mode: 'merge' | 'replace'\n created: number\n overwritten: number\n replacedTotal?: number\n /** The backup file written BEFORE a replace wiped the live ledger. */\n backupFile?: string\n}\n\n/** Diff-viewer response (0.4.0). */\nexport type DiffResponse = { diff: string; truncated: boolean }\n\n/** One task (full record) response. */\nexport type TaskResponse = TaskRecord\n\n/** Summary response used by list-ish endpoints. */\nexport type SummaryResponse = { tasks: TaskSummary[] }\n\n// ---------------------------------------------------------------------------\n// SSE\n// ---------------------------------------------------------------------------\n\n/** Change frame pushed on every committed ledger mutation. */\nexport type ChangeEvent = {\n revision: number\n kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded' | 'settings-updated' | 'ledger-replaced'\n tasks: TaskSummary[]\n}\n"],"mappings":";;AAYA,MAAa,eAAe;;AAG5B,MAAa,WAAW"}
|
|
1
|
+
{"version":3,"file":"api.js","names":[],"sources":["../../src/shared/api.ts"],"sourcesContent":["/**\n * Wire contract for the /taskboard host routes: the JSON envelope,\n * request/response shapes, and SSE event payloads shared by the host routes\n * and the browser client.\n *\n * @module dsh-taskboard/shared/api\n */\nimport type { BoardSettings, TaskLedger, TaskModel, TaskRecord, TaskSummary } from './protocol.ts'\n\nexport type { TaskModel, TaskRecord }\n\n/** Route prefix on the shared DSH webserver (same origin as the GUI). */\nexport const ROUTE_PREFIX = '/dsh-taskboard'\n\n/** SSE stream path (exact route; longest-prefix wins keep it disjoint). */\nexport const SSE_PATH = '/dsh-taskboard/events'\n\n/** Stable error codes (mirror the tool-level codes plus HTTP mapping). */\nexport type ApiErrorCode =\n | 'invalid_input'\n | 'not_found'\n | 'version_conflict'\n | 'invalid_transition'\n | 'forbidden'\n | 'internal'\n\n/** Success envelope. */\nexport type ApiOk<T> = { ok: true; value: T }\n\n/** Failure envelope. */\nexport type ApiFail = { ok: false; error: { code: ApiErrorCode; message: string } }\n\n/** The envelope either way. */\nexport type ApiResult<T> = ApiOk<T> | ApiFail\n\n// ---------------------------------------------------------------------------\n// payloads\n// ---------------------------------------------------------------------------\n\n/** Full-state response (the reconnect baseline after an SSE gap). */\nexport type StateResponse = TaskLedger\n\n/** Workspace listing for the UI pickers. */\nexport type WorkspaceView = { id: string; path: string; title: string; sessionCount: number; gitAvailable?: boolean }\n\n/** Create-task request body (actor is always the GUI user). */\nexport type CreateTaskBody = {\n title: string\n workspaceId: string\n urgency: string\n description?: string\n prompt?: string\n execution?: { mode?: string; cron?: string }\n model?: TaskModel\n /** Code isolation for executions ('worktree' | 'none'); omitted = default. */\n isolation?: string\n /** Agent preset for execution sessions; omitted = deployment default. */\n presetId?: string\n /** Execution permission preset ('workspace-write' | 'read-only' | 'danger-full-access'); omitted = default. */\n permission?: string\n /** Acceptance checklist item texts (host mints ids, all unchecked). */\n checklist?: string[]\n}\n\n/** Update-task request body (ifVersion mandatory). */\nexport type UpdateTaskBody = {\n ifVersion: number\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n blocked?: boolean\n /** Rebind the task to another project (GUI owner surface only). */\n workspaceId?: string\n execution?: { mode?: string; cron?: string }\n model?: TaskModel | null\n /** Change isolation; locked once the task has execution history. */\n isolation?: string\n /** Change the execution preset (takes effect on the next run). */\n presetId?: string | null\n /** Change the execution permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access'). */\n permission?: string | null\n /** Replace the whole checklist (GUI owner surface); null clears it. */\n checklist?: unknown\n}\n\n/** Move-task request body (ifVersion mandatory; the user MAY move to done). */\nexport type MoveTaskBody = { ifVersion: number; status: string }\n\n/**\n * Quick-reject request body (card ✗ button): move back to todo plus an\n * optional user comment, committed as ONE ledger mutation so a failed move\n * can never strand an orphan comment.\n */\nexport type RejectTaskBody = { ifVersion: number; body?: string }\n\n/** Comment request body. */\nexport type CommentBody = { body: string }\n\n/** Delete request body (purge=true physically removes a trashed task). */\nexport type DeleteTaskBody = { ifVersion?: number; purge?: boolean }\n\n/** Run request body; `reuse: true` = 续跑 (keep a live worktree as-is). */\nexport type RunTaskBody = { reuse?: boolean }\n\n/** Merge outcome: `noop: true` = the branch had no commits over HEAD (nothing merged). */\nexport type MergeBranchResponse = { merged: boolean; noop?: boolean; branch: string }\n\n/** Remove a task's worktree; optionally delete its branch too. */\nexport type WorktreeRemoveBody = { deleteBranch?: boolean }\n\n/** One orphan worktree directory (exists on disk, owned by no live task). */\nexport type OrphanWorktree = { workspaceId: string; workspacePath: string; taskId: string; path: string }\n\n/** A git-enabled workspace whose .gitignore does not cover the worktree dir. */\nexport type GitignoreSuggestion = { workspaceId: string; workspacePath: string }\n\n/** Health-diagnostics response (⚙ panel). */\nexport type DiagnosticsResponse = {\n revision: number\n tasks: number\n /** Executions currently marked `running`. */\n staleRunning: number\n /** Worktree directories whose task no longer exists in the ledger. */\n orphanWorktrees: OrphanWorktree[]\n /** Git workspaces whose .gitignore does not ignore the worktree dir. */\n gitIgnoreSuggestions: GitignoreSuggestion[]\n}\n\n/** Fields a task template may prefill (0.4.0). */\nexport type TaskTemplateSpec = {\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n execution?: { mode?: string; cron?: string }\n model?: TaskModel\n isolation?: string\n presetId?: string\n /** Execution permission preset (0.5.5). */\n permission?: string\n /** Checklist item texts (host mints ids at create time). */\n checklist?: string[]\n}\n\n/** One reusable task template (0.4.0). */\nexport type TaskTemplate = {\n id: string\n name: string\n task: TaskTemplateSpec\n /** Seeded built-in templates (kept on load, deletable like any other). */\n builtin?: boolean\n createdAt: number\n updatedAt: number\n}\n\n/** Templates listing response. */\nexport type TemplatesResponse = { templates: TaskTemplate[] }\n\n/** Board-settings response (0.5.0; absent fields follow factory defaults). */\nexport type SettingsResponse = BoardSettings\n\n/** Update-board-settings request body (0.5.0; whole-object replace semantics). */\nexport type UpdateSettingsBody = {\n /** Default code isolation for NEW tasks ('worktree' | 'none'). */\n defaultIsolation?: string\n /** Automatically capture external workspace sessions into the taskboard. */\n syncExternalSessions?: boolean\n /** Default permission preset for NEW tasks ('workspace-write' | 'read-only' | 'danger-full-access'). */\n defaultPermission?: string\n}\n\n/** Prompt completion item for skills and slash commands (0.5.5). */\nexport type PromptCompletionItem = {\n name: string\n kind: 'skill' | 'command'\n description?: string\n hint?: string\n}\n\n/** Prompt completions response (0.5.5). */\nexport type PromptCompletionsResponse = {\n commands: PromptCompletionItem[]\n skills: PromptCompletionItem[]\n}\n\n/** Model item in catalog (0.5.5). */\nexport type CatalogModelItem = {\n provider: string\n model: string\n name?: string\n description?: string\n reasoning?: {\n efforts: Array<{ id: string; name: string; description?: string }>\n defaultEffort?: string\n }\n}\n\n/** Preset item in catalog (0.5.5). */\nexport type CatalogPresetItem = {\n id: string\n name?: string\n}\n\n/** Model and preset catalog response (0.5.5). */\nexport type ModelCatalogResponse = {\n models: CatalogModelItem[]\n presets: CatalogPresetItem[]\n defaultPresetId?: string\n}\n\n/** Import dry-run response (0.4.0): every task classified, nothing written. */\nexport type ImportPreviewResponse = {\n plan: {\n create: Array<{ id: string; title: string; status: string }>\n overwrite: Array<{ id: string; title: string; status: string }>\n invalid: Array<{ id?: string; reason: string }>\n }\n}\n\n/** Import commit response. */\nexport type ImportCommitResponse = {\n mode: 'merge' | 'replace'\n created: number\n overwritten: number\n replacedTotal?: number\n /** The backup file written BEFORE a replace wiped the live ledger. */\n backupFile?: string\n}\n\n/** Diff-viewer response (0.4.0). */\nexport type DiffResponse = { diff: string; truncated: boolean }\n\n/** One task (full record) response. */\nexport type TaskResponse = TaskRecord\n\n/** Summary response used by list-ish endpoints. */\nexport type SummaryResponse = { tasks: TaskSummary[] }\n\n// ---------------------------------------------------------------------------\n// SSE\n// ---------------------------------------------------------------------------\n\n/** Change frame pushed on every committed ledger mutation. */\nexport type ChangeEvent = {\n revision: number\n kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded' | 'settings-updated' | 'ledger-replaced'\n tasks: TaskSummary[]\n}\n"],"mappings":";;AAYA,MAAa,eAAe;;AAG5B,MAAa,WAAW"}
|
package/lib/shared/protocol.js
CHANGED
|
@@ -75,6 +75,17 @@ function asIsolation(raw) {
|
|
|
75
75
|
function effectiveIsolation(task) {
|
|
76
76
|
return task.isolation === void 0 ? DEFAULT_ISOLATION : task.isolation;
|
|
77
77
|
}
|
|
78
|
+
/** Factory default permission preset (0.5.5). */
|
|
79
|
+
const DEFAULT_PERMISSION = "workspace-write";
|
|
80
|
+
/** Validate and normalize a permission string into a valid {@link PermissionMode}. */
|
|
81
|
+
function asPermission(raw) {
|
|
82
|
+
if (typeof raw !== "string") return DEFAULT_PERMISSION;
|
|
83
|
+
const normalized = raw.trim();
|
|
84
|
+
if (normalized === "workspace-write" || normalized === "workspaceWrite") return "workspace-write";
|
|
85
|
+
if (normalized === "read-only" || normalized === "readOnly") return "read-only";
|
|
86
|
+
if (normalized === "danger-full-access" || normalized === "fullAccess") return "danger-full-access";
|
|
87
|
+
throw new Error("permission must be 'workspace-write', 'read-only', or 'danger-full-access'");
|
|
88
|
+
}
|
|
78
89
|
/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */
|
|
79
90
|
function asBoardSettings(raw) {
|
|
80
91
|
if (typeof raw !== "object" || raw === null) throw new Error("board settings must be an object");
|
|
@@ -84,12 +95,25 @@ function asBoardSettings(raw) {
|
|
|
84
95
|
if (typeof e.defaultIsolation !== "string") throw new Error("defaultIsolation must be 'worktree' or 'none'");
|
|
85
96
|
out.defaultIsolation = asIsolation(e.defaultIsolation);
|
|
86
97
|
}
|
|
98
|
+
if (e.syncExternalSessions !== void 0) {
|
|
99
|
+
if (typeof e.syncExternalSessions !== "boolean") throw new Error("syncExternalSessions must be a boolean");
|
|
100
|
+
out.syncExternalSessions = e.syncExternalSessions;
|
|
101
|
+
}
|
|
102
|
+
if (e.defaultPermission !== void 0) out.defaultPermission = asPermission(e.defaultPermission);
|
|
87
103
|
return out;
|
|
88
104
|
}
|
|
89
105
|
/** The effective default isolation for NEW tasks (board setting → factory default). */
|
|
90
106
|
function defaultIsolationOf(settings) {
|
|
91
107
|
return settings?.defaultIsolation ?? "none";
|
|
92
108
|
}
|
|
109
|
+
/** The effective external session sync switch (board setting → factory default false). */
|
|
110
|
+
function defaultSyncExternalSessionsOf(settings) {
|
|
111
|
+
return settings?.syncExternalSessions ?? false;
|
|
112
|
+
}
|
|
113
|
+
/** The effective default permission preset for NEW tasks (board setting → factory default 'workspace-write'). */
|
|
114
|
+
function defaultPermissionOf(settings) {
|
|
115
|
+
return settings?.defaultPermission ?? "workspace-write";
|
|
116
|
+
}
|
|
93
117
|
/**
|
|
94
118
|
* Parse a five-field cron expression. Supported field syntax: star, star/step
|
|
95
119
|
* (`* / n` without spaces), a single number, an `a-b` range, and comma lists
|
|
@@ -551,6 +575,7 @@ function validateImportedTask(raw, now) {
|
|
|
551
575
|
...typeof e.model === "object" && e.model !== null ? { model: normalizeModel(e.model) } : {},
|
|
552
576
|
...typeof e.isolation === "string" && (e.isolation === "worktree" || e.isolation === "none") ? { isolation: e.isolation } : {},
|
|
553
577
|
...typeof e.presetId === "string" && e.presetId.trim().length > 0 ? { presetId: e.presetId.trim() } : {},
|
|
578
|
+
...typeof e.permission === "string" ? { permission: asPermission(e.permission) } : {},
|
|
554
579
|
...Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {},
|
|
555
580
|
...typeof e.branch === "string" ? { branch: e.branch } : {},
|
|
556
581
|
...status === "in_progress" && typeof e.claimedBy === "string" ? { claimedBy: e.claimedBy } : {},
|
|
@@ -647,6 +672,7 @@ function summarize(task) {
|
|
|
647
672
|
executionMode: task.execution.mode,
|
|
648
673
|
nextRunAt: task.execution.nextRunAt,
|
|
649
674
|
model: task.model,
|
|
675
|
+
permission: task.permission,
|
|
650
676
|
version: task.version,
|
|
651
677
|
claimOwner: isClaimedBy(task),
|
|
652
678
|
commentCount: task.comments.length,
|
|
@@ -656,6 +682,6 @@ function summarize(task) {
|
|
|
656
682
|
};
|
|
657
683
|
}
|
|
658
684
|
//#endregion
|
|
659
|
-
export { ALL_STATUSES, DEFAULT_ISOLATION, MAIN_STATUSES, SECONDARY_STATUSES, URGENCIES, asBoardSettings, asIsolation, asStatus, asUrgency, canTransition, checklistFromTexts, checklistProgress, defaultIsolationOf, effectiveIsolation, effectivePrompt, emptyLedger, isClaim, isClaimedBy, isPlausibleTaskRecord, isValidTaskId, newChecklistItemId, newCommentId, newExecutionId, newTaskId, nextCronTime, normalizeBody, normalizeChecklist, normalizeChecklistText, normalizeExecution, normalizeExecutionReport, normalizeModel, normalizePrompt, normalizeTitle, parseCron, pruneExecutions, summarize, syncClaim, validateImportedTask, validateLedgerImport };
|
|
685
|
+
export { ALL_STATUSES, DEFAULT_ISOLATION, DEFAULT_PERMISSION, MAIN_STATUSES, SECONDARY_STATUSES, URGENCIES, asBoardSettings, asIsolation, asPermission, asStatus, asUrgency, canTransition, checklistFromTexts, checklistProgress, defaultIsolationOf, defaultPermissionOf, defaultSyncExternalSessionsOf, effectiveIsolation, effectivePrompt, emptyLedger, isClaim, isClaimedBy, isPlausibleTaskRecord, isValidTaskId, newChecklistItemId, newCommentId, newExecutionId, newTaskId, nextCronTime, normalizeBody, normalizeChecklist, normalizeChecklistText, normalizeExecution, normalizeExecutionReport, normalizeModel, normalizePrompt, normalizeTitle, parseCron, pruneExecutions, summarize, syncClaim, validateImportedTask, validateLedgerImport };
|
|
660
686
|
|
|
661
687
|
//# sourceMappingURL=protocol.js.map
|