@kafca/agentdock 0.1.23 → 0.1.25
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 +3 -0
- package/dist/renderer/assets/{Badge-C2K5apfV.js → Badge-Bn2h-Qkf.js} +1 -1
- package/dist/renderer/assets/{Chat-DI2YeVKb.js → Chat-Cgo9aneg.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-DMAr15Oi.js → ChatMarkdown-CYzHfSiR.js} +1 -1
- package/dist/renderer/assets/{Config-7rP3EuUf.js → Config-DljB2-eP.js} +1 -1
- package/dist/renderer/assets/{CronList-81QslYE8.js → CronList-DeVbiY9a.js} +1 -1
- package/dist/renderer/assets/{Dashboard-Db2T7rSZ.js → Dashboard-fSqx5sm8.js} +2 -2
- package/dist/renderer/assets/{EmptyState-BBJ3cTvB.js → EmptyState-DgqKDwlq.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-Dz_6flG3.js → KnowledgeDetail-u8s2SmPV.js} +1 -1
- package/dist/renderer/assets/{KnowledgeHome-DIh046mN.js → KnowledgeHome-BdQw8nbY.js} +1 -1
- package/dist/renderer/assets/{Logs-BXBtHJLf.js → Logs-DSbndMpy.js} +1 -1
- package/dist/renderer/assets/{Modal-BB2Av_Xs.js → Modal-0d0EmAuK.js} +1 -1
- package/dist/renderer/assets/{Page-DPSdvMgU.js → Page-CVbOH_Kh.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-C6EWGvGg.js → ProjectDetail-C91tF7uA.js} +1 -1
- package/dist/renderer/assets/{ProjectList-BBo5ygnS.js → ProjectList-D78Pc-BQ.js} +1 -1
- package/dist/renderer/assets/{Select-CNX-Tx_O.js → Select-BbKzAIuO.js} +1 -1
- package/dist/renderer/assets/{SessionChat-BIn46jqZ.js → SessionChat-BERvL5e4.js} +1 -1
- package/dist/renderer/assets/{SessionList-CZt6f1Rd.js → SessionList-Bomz335M.js} +1 -1
- package/dist/renderer/assets/ThreadChat-CLr3amU2.js +23 -0
- package/dist/renderer/assets/{Workspace-C_4tdpjX.js → Workspace-In91z4W2.js} +1 -1
- package/dist/renderer/assets/{arrow-left-cHr5wkpq.js → arrow-left-FBnVKKBl.js} +1 -1
- package/dist/renderer/assets/{book-open-4nx5C8T-.js → book-open-4_CuP6Sa.js} +1 -1
- package/dist/renderer/assets/{check-exTS-R-x.js → check-vsoVjvac.js} +1 -1
- package/dist/renderer/assets/{chevron-down-DSvxXQa0.js → chevron-down-DFpmFTJW.js} +1 -1
- package/dist/renderer/assets/{circle-DyvEovdw.js → circle-vRULegER.js} +1 -1
- package/dist/renderer/assets/desktop-BkMW-h5p.js +4 -0
- package/dist/renderer/assets/{desktop-B-KVT10D.js → desktop-ul0Tpzyc.js} +1 -1
- package/dist/renderer/assets/{funnel-DA9qVdTy.js → funnel-Gu0E_hJ_.js} +1 -1
- package/dist/renderer/assets/{heart-DuEV0ZRJ.js → heart-DsdofOHf.js} +1 -1
- package/dist/renderer/assets/{index-Biz-aexG.js → index-BD3aCyBa.js} +3 -3
- package/dist/renderer/assets/{index-BgflqkCN.js → index-pJUFobU7.js} +1 -1
- package/dist/renderer/assets/{pencil-DBDxeNmO.js → pencil-DbKmV8nD.js} +1 -1
- package/dist/renderer/assets/{play-IzlP83_7.js → play-BDMsRxSm.js} +1 -1
- package/dist/renderer/assets/{plug-CuT-qFnW.js → plug-t2dMl78-.js} +1 -1
- package/dist/renderer/assets/{plus-DEqgkn63.js → plus-BUb6zOAe.js} +1 -1
- package/dist/renderer/assets/projects-4F6D_Q8t.js +1 -0
- package/dist/renderer/assets/{save-D4sRo07y.js → save-C-_5rQdv.js} +1 -1
- package/dist/renderer/assets/{search-DbPZLuV8.js → search-R3ekHFPM.js} +1 -1
- package/dist/renderer/assets/{server-D1jUQmwN.js → server-BTuvHf5n.js} +1 -1
- package/dist/renderer/assets/{session-utils-Aife8ep2.js → session-utils-ClX6VPFG.js} +1 -1
- package/dist/renderer/assets/{sessions-DaH3H7gC.js → sessions-AFKIC-ub.js} +1 -1
- package/dist/renderer/assets/{shield-check-BGwUFWNr.js → shield-check-CqMW8CHe.js} +1 -1
- package/dist/renderer/assets/{trash-2-CIFBioBa.js → trash-2-BNLcHyUw.js} +1 -1
- package/dist/renderer/assets/{user-CnKcvHdo.js → user-BCCN8FYK.js} +1 -1
- package/dist/renderer/assets/{wifi-off-ClO53WGa.js → wifi-off-BsNsx3oX.js} +1 -1
- package/dist/renderer/index.html +1 -1
- package/dist-electron/electron/agent-runtime-detector.test.js +64 -0
- package/dist-electron/electron/architecture-docs.test.js +56 -0
- package/dist-electron/electron/channel-content.test.js +37 -0
- package/dist-electron/electron/lac-cli.test.js +297 -4
- package/dist-electron/electron/local-core-acp-content.test.js +39 -0
- package/dist-electron/electron/local-core-contracts.test.js +67 -0
- package/dist-electron/electron/local-core-refactor.test.js +789 -3
- package/dist-electron/electron/plugin-kernel.test.js +108 -0
- package/dist-electron/electron/renderer-packaging.test.js +77 -0
- package/dist-electron/electron/workspace-task-store.test.js +181 -0
- package/dist-electron/packages/contracts/src/local-core.js +110 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +67 -6
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +25 -4
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-permission-lifecycle.js +76 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-progress.js +201 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +3 -2
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +29 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +74 -46
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +112 -210
- package/dist-electron/services/local-ai-core/src/acp/local-core-slash-commands.js +60 -0
- package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +5 -48
- package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +83 -28
- package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +11 -0
- package/dist-electron/services/local-ai-core/src/channel/shared/content.js +23 -0
- package/dist-electron/services/local-ai-core/src/channel/shared/file-utils.js +13 -1
- package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +31 -29
- package/dist-electron/services/local-ai-core/src/cli/lac.js +22 -17
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +1 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +9 -0
- package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +221 -10
- package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +39 -9
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +1 -0
- package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-service.js +1 -0
- package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +439 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +208 -303
- package/dist-electron/services/local-ai-core/src/scheduler/job-id.js +15 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +30 -5
- package/dist-electron/shared/desktop.js +34 -8
- package/dist-electron/src/pages/Threads/thread-chat-message-blocks.js +187 -0
- package/dist-electron/src/pages/Threads/thread-chat-model.js +58 -0
- package/dist-electron/src/pages/Threads/thread-chat-page-state.js +96 -0
- package/dist-electron/src/pages/Threads/thread-chat-permission.js +18 -0
- package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +347 -0
- package/package.json +4 -2
- package/dist/renderer/assets/ThreadChat-BotD0gKQ.js +0 -23
- package/dist/renderer/assets/desktop-CVibRyAw.js +0 -4
- package/dist/renderer/assets/projects-DDtDN1rc.js +0 -1
|
@@ -155,6 +155,7 @@ function plugin(id, dependsOn = []) {
|
|
|
155
155
|
'qoder',
|
|
156
156
|
'iflow',
|
|
157
157
|
'localcore-acp',
|
|
158
|
+
'pi',
|
|
158
159
|
'opencode',
|
|
159
160
|
'codex',
|
|
160
161
|
'claudecode',
|
|
@@ -188,6 +189,7 @@ function plugin(id, dependsOn = []) {
|
|
|
188
189
|
'qoder',
|
|
189
190
|
'iflow',
|
|
190
191
|
'localcore-acp',
|
|
192
|
+
'pi',
|
|
191
193
|
'opencode',
|
|
192
194
|
'codex',
|
|
193
195
|
'claudecode',
|
|
@@ -243,6 +245,104 @@ type = "codex"
|
|
|
243
245
|
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
244
246
|
}
|
|
245
247
|
});
|
|
248
|
+
(0, node_test_1.default)('pi agent runtime routes projects through bundled pi ACP and coding agent', async () => {
|
|
249
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-kernel-'));
|
|
250
|
+
try {
|
|
251
|
+
const runtime = (0, bootstrap_js_2.bootstrapLocalCoreRuntime)({
|
|
252
|
+
userDataPath,
|
|
253
|
+
enableKnowledge: false,
|
|
254
|
+
});
|
|
255
|
+
await runtime.state.saveRawConfigFile(`
|
|
256
|
+
[[projects]]
|
|
257
|
+
name = "pi-workspace"
|
|
258
|
+
|
|
259
|
+
[projects.agent]
|
|
260
|
+
type = "pi"
|
|
261
|
+
|
|
262
|
+
[[projects.agent.providers]]
|
|
263
|
+
name = "openai"
|
|
264
|
+
api_key = "test-openai-key"
|
|
265
|
+
|
|
266
|
+
[projects.agent.options.env]
|
|
267
|
+
OPENAI_API_KEY = "override-openai-key"
|
|
268
|
+
`);
|
|
269
|
+
const configState = await runtime.state.readConfigFile();
|
|
270
|
+
const project = configState.parsed?.projects?.find((entry) => entry.name === 'pi-workspace');
|
|
271
|
+
const piRuntime = runtime.agentRuntimes.find((entry) => entry.agentType === 'pi');
|
|
272
|
+
const route = project ? piRuntime?.createRoute(configState, project) : null;
|
|
273
|
+
strict_1.default.equal(route?.agentType, 'pi');
|
|
274
|
+
strict_1.default.equal(route?.config.command, process.execPath);
|
|
275
|
+
strict_1.default.match(route?.config.args[0] || '', /pi-acp[/\\]dist[/\\]index\.js$/);
|
|
276
|
+
strict_1.default.match(route?.config.env.PI_ACP_PI_COMMAND || '', /@mariozechner[/\\]pi-coding-agent[/\\]dist[/\\]cli\.js$/);
|
|
277
|
+
strict_1.default.equal(route?.config.env.OPENAI_API_KEY, 'override-openai-key');
|
|
278
|
+
strict_1.default.match(route?.config.env.PI_CODING_AGENT_DIR || '', /[\\/]\.pi-agent[\\/]pi-workspace$/);
|
|
279
|
+
strict_1.default.deepEqual(await runtime.workspaceRouter.listWorkspaces(), [
|
|
280
|
+
{
|
|
281
|
+
id: 'pi-workspace',
|
|
282
|
+
name: 'pi-workspace',
|
|
283
|
+
agentType: 'pi',
|
|
284
|
+
platforms: [],
|
|
285
|
+
sessionsCount: 0,
|
|
286
|
+
heartbeatEnabled: false,
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
await runtime.stop();
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
(0, node_test_1.default)('pi agent runtime writes provider auth and default model into Pi config dir', async () => {
|
|
296
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-kernel-'));
|
|
297
|
+
try {
|
|
298
|
+
const runtime = (0, bootstrap_js_2.bootstrapLocalCoreRuntime)({
|
|
299
|
+
userDataPath,
|
|
300
|
+
enableKnowledge: false,
|
|
301
|
+
});
|
|
302
|
+
await runtime.state.saveRawConfigFile(`
|
|
303
|
+
[[projects]]
|
|
304
|
+
name = "deepseek-workspace"
|
|
305
|
+
|
|
306
|
+
[projects.agent]
|
|
307
|
+
type = "pi"
|
|
308
|
+
|
|
309
|
+
[[projects.agent.providers]]
|
|
310
|
+
name = "DeepSeek"
|
|
311
|
+
api_key = "test-deepseek-key"
|
|
312
|
+
base_url = "https://api.deepseek.com"
|
|
313
|
+
model = "deepseek-v4-flash"
|
|
314
|
+
`);
|
|
315
|
+
const configState = await runtime.state.readConfigFile();
|
|
316
|
+
const project = configState.parsed?.projects?.find((entry) => entry.name === 'deepseek-workspace');
|
|
317
|
+
const piRuntime = runtime.agentRuntimes.find((entry) => entry.agentType === 'pi');
|
|
318
|
+
const route = project ? piRuntime?.createRoute(configState, project) : null;
|
|
319
|
+
const piAgentDir = route?.config.env.PI_CODING_AGENT_DIR || '';
|
|
320
|
+
strict_1.default.equal(route?.config.model, 'deepseek-v4-flash');
|
|
321
|
+
strict_1.default.equal(route?.config.env.DEEPSEEK_API_KEY, 'test-deepseek-key');
|
|
322
|
+
strict_1.default.match(piAgentDir, /[\\/]\.pi-agent[\\/]deepseek-workspace$/);
|
|
323
|
+
strict_1.default.deepEqual(JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(piAgentDir, 'auth.json'), 'utf8')), {
|
|
324
|
+
deepseek: { type: 'api_key', key: 'test-deepseek-key' },
|
|
325
|
+
});
|
|
326
|
+
strict_1.default.deepEqual(JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(piAgentDir, 'settings.json'), 'utf8')), {
|
|
327
|
+
defaultProvider: 'deepseek',
|
|
328
|
+
defaultModel: 'deepseek-v4-flash',
|
|
329
|
+
quietStartup: true,
|
|
330
|
+
});
|
|
331
|
+
strict_1.default.deepEqual(JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(piAgentDir, 'models.json'), 'utf8')), {
|
|
332
|
+
providers: {
|
|
333
|
+
deepseek: {
|
|
334
|
+
name: 'DeepSeek',
|
|
335
|
+
baseUrl: 'https://api.deepseek.com',
|
|
336
|
+
apiKey: 'test-deepseek-key',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
await runtime.stop();
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
246
346
|
(0, node_test_1.default)('runtime bootstrap knowledge capabilities come from the selected provider plugin', () => {
|
|
247
347
|
const enabledUserDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-kernel-'));
|
|
248
348
|
const disabledUserDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-kernel-'));
|
|
@@ -619,6 +719,7 @@ type = "codex"
|
|
|
619
719
|
defaultCollection: 'personal_knowledge',
|
|
620
720
|
},
|
|
621
721
|
plugins: {
|
|
722
|
+
'builtin.agent-pi': { enabled: false },
|
|
622
723
|
'builtin.agent-claudecode': { enabled: false },
|
|
623
724
|
},
|
|
624
725
|
}), 'utf8');
|
|
@@ -631,9 +732,16 @@ name = "claude-workspace"
|
|
|
631
732
|
|
|
632
733
|
[projects.agent]
|
|
633
734
|
type = "claudecode"
|
|
735
|
+
|
|
736
|
+
[[projects]]
|
|
737
|
+
name = "pi-workspace"
|
|
738
|
+
|
|
739
|
+
[projects.agent]
|
|
740
|
+
type = "pi"
|
|
634
741
|
`);
|
|
635
742
|
strict_1.default.deepEqual(runtime.agentRuntimes.map((entry) => entry.agentType), ['localcore-acp', 'opencode', 'codex']);
|
|
636
743
|
strict_1.default.equal(runtime.kernel.getCapabilitySnapshot().snapshot.agents.some((capability) => capability.agentType === 'claudecode'), false);
|
|
744
|
+
strict_1.default.equal(runtime.kernel.getCapabilitySnapshot().snapshot.agents.some((capability) => capability.agentType === 'pi'), false);
|
|
637
745
|
strict_1.default.deepEqual(await runtime.workspaceRouter.listWorkspaces(), []);
|
|
638
746
|
await strict_1.default.rejects(() => runtime.workspaceRouter.listThreads('claude-workspace'), /Workspace "claude-workspace" is not configured as a Local AI Core ACP workspace/);
|
|
639
747
|
await runtime.stop();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
11
|
+
const rootDir = node_process_1.default.cwd();
|
|
12
|
+
(0, node_test_1.default)('production package config includes renderer build output', () => {
|
|
13
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, 'package.json'), 'utf8'));
|
|
14
|
+
strict_1.default.ok(packageJson.files?.includes('dist/renderer/**'), 'npm package files must include dist/renderer/**');
|
|
15
|
+
strict_1.default.ok(packageJson.build?.files?.includes('dist/renderer/**'), 'electron-builder files must include dist/renderer/**');
|
|
16
|
+
});
|
|
17
|
+
(0, node_test_1.default)('release validation scripts keep local and candidate gates intact', () => {
|
|
18
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, 'package.json'), 'utf8'));
|
|
19
|
+
const testScript = packageJson.scripts?.test || '';
|
|
20
|
+
const smokeScript = packageJson.scripts?.['e2e:smoke'] || '';
|
|
21
|
+
strict_1.default.match(testScript, /\bpnpm build:renderer\b/, 'pnpm test must build renderer assets');
|
|
22
|
+
strict_1.default.match(testScript, /\bpnpm build:electron\b/, 'pnpm test must compile Electron and Local AI Core tests');
|
|
23
|
+
strict_1.default.match(testScript, /\bnode --test\b/, 'pnpm test must run the Node test suite');
|
|
24
|
+
strict_1.default.match(testScript, /dist-electron\/electron\/\*\.test\.js/, 'pnpm test must include Electron tests');
|
|
25
|
+
strict_1.default.match(testScript, /dist-electron\/packages\/knowledge-api\/test\/\*\.test\.js/, 'pnpm test must include package tests');
|
|
26
|
+
strict_1.default.match(testScript, /dist-electron\/src\/pages\/Threads\/thread-chat-permission\.test\.js/, 'pnpm test must include renderer state tests');
|
|
27
|
+
const buildIndex = smokeScript.indexOf('pnpm build');
|
|
28
|
+
const smokeIndex = smokeScript.indexOf('node scripts/e2e-smoke.mjs');
|
|
29
|
+
strict_1.default.ok(buildIndex >= 0, 'pnpm e2e:smoke must start from a production build');
|
|
30
|
+
strict_1.default.ok(smokeIndex > buildIndex, 'pnpm e2e:smoke must run smoke checks after the production build');
|
|
31
|
+
});
|
|
32
|
+
(0, node_test_1.default)('production renderer build has a loadable entry document and assets', () => {
|
|
33
|
+
const rendererDir = (0, node_path_1.join)(rootDir, 'dist', 'renderer');
|
|
34
|
+
const indexPath = (0, node_path_1.join)(rendererDir, 'index.html');
|
|
35
|
+
strict_1.default.ok((0, node_fs_1.existsSync)(indexPath), 'dist/renderer/index.html must exist after production build');
|
|
36
|
+
const indexHtml = (0, node_fs_1.readFileSync)(indexPath, 'utf8');
|
|
37
|
+
strict_1.default.match(indexHtml, /<div id="root"><\/div>/, 'renderer entry must mount into #root');
|
|
38
|
+
const assetReferences = [...indexHtml.matchAll(/(?:src|href)="\.\/(assets\/[^"]+)"/g)].map((match) => match[1]);
|
|
39
|
+
strict_1.default.ok(assetReferences.some((asset) => asset.endsWith('.js')), 'renderer entry must reference a JS asset');
|
|
40
|
+
strict_1.default.ok(assetReferences.some((asset) => asset.endsWith('.css')), 'renderer entry must reference a CSS asset');
|
|
41
|
+
for (const asset of assetReferences) {
|
|
42
|
+
strict_1.default.ok((0, node_fs_1.existsSync)((0, node_path_1.join)(rendererDir, asset)), `renderer asset must exist: ${asset}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
(0, node_test_1.default)('release workflow keeps validation artifacts separate from formal releases', () => {
|
|
46
|
+
const ciWorkflow = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, '.github', 'workflows', 'ci.yml'), 'utf8');
|
|
47
|
+
const releaseWorkflow = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, '.github', 'workflows', 'release.yml'), 'utf8');
|
|
48
|
+
const releaseDocs = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, 'docs', 'operations', 'release-workflow.md'), 'utf8');
|
|
49
|
+
strict_1.default.match(ciWorkflow, /pull_request:/, 'CI must run before merge');
|
|
50
|
+
strict_1.default.match(ciWorkflow, /branches:\s*\n\s*- main/, 'CI must run on main pushes');
|
|
51
|
+
strict_1.default.match(ciWorkflow, /needs: test/, 'main branch packaging must wait for tests');
|
|
52
|
+
strict_1.default.match(ciWorkflow, /--publish never/, 'main branch artifacts must not publish formal releases');
|
|
53
|
+
strict_1.default.match(ciWorkflow, /retention-days: 14/, 'main branch validation artifacts must have bounded retention');
|
|
54
|
+
strict_1.default.match(releaseWorkflow, /tags:\s*\n\s*- 'v\*'/, 'formal releases must be tied to version tags');
|
|
55
|
+
strict_1.default.match(releaseWorkflow, /Run tests[\s\S]*?run: pnpm test/, 'release packaging must run the fast gate');
|
|
56
|
+
strict_1.default.match(releaseWorkflow, /Build app[\s\S]*?run: pnpm build/, 'release packaging must run a production build');
|
|
57
|
+
strict_1.default.match(releaseWorkflow, /--publish always/, 'release artifacts must publish only from the release workflow');
|
|
58
|
+
strict_1.default.match(releaseWorkflow, /tag_version.*package_version/s, 'tag releases must verify the package version');
|
|
59
|
+
strict_1.default.match(releaseDocs, /validation only/i, 'release docs must describe main artifacts as validation only');
|
|
60
|
+
strict_1.default.match(releaseDocs, /not formal releases/i, 'release docs must separate main artifacts from formal releases');
|
|
61
|
+
});
|
|
62
|
+
(0, node_test_1.default)('production smoke gate launches the built app and checks runtime capabilities', () => {
|
|
63
|
+
const smokeScript = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, 'scripts', 'e2e-smoke.mjs'), 'utf8');
|
|
64
|
+
const mainSource = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rootDir, 'electron', 'main.ts'), 'utf8');
|
|
65
|
+
strict_1.default.match(smokeScript, /scripts\/launch-electron\.mjs/, 'smoke gate must launch the production Electron entry');
|
|
66
|
+
strict_1.default.match(smokeScript, /AI_WORKSTATION_SMOKE_OUTPUT/, 'smoke gate must wait for an app-written smoke snapshot');
|
|
67
|
+
strict_1.default.match(smokeScript, /AI_WORKSTATION_USER_DATA_DIR/, 'smoke gate must isolate app data per scenario');
|
|
68
|
+
strict_1.default.match(smokeScript, /runScenario\('default'\)/, 'smoke gate must cover the default production startup path');
|
|
69
|
+
strict_1.default.match(smokeScript, /runScenario\('bootstrap-error'/, 'smoke gate must cover degraded bootstrap behavior');
|
|
70
|
+
strict_1.default.match(smokeScript, /runScenario\('degraded-plugin'/, 'smoke gate must cover plugin diagnostic behavior');
|
|
71
|
+
strict_1.default.match(smokeScript, /localcore-acp/, 'smoke gate must verify the agent capability snapshot');
|
|
72
|
+
strict_1.default.match(smokeScript, /pluginDiagnostics/, 'smoke gate must verify plugin diagnostics');
|
|
73
|
+
strict_1.default.match(mainSource, /appResourcePath\('dist', 'renderer', 'index\.html'\)/, 'production app must load built renderer assets');
|
|
74
|
+
strict_1.default.match(mainSource, /Renderer build output was not found/, 'production app must fail loudly when renderer assets are missing');
|
|
75
|
+
strict_1.default.match(mainSource, /\/api\/local\/v1\/capabilities\/snapshot/, 'smoke mode must check runtime capabilities through the app');
|
|
76
|
+
strict_1.default.match(mainSource, /\/api\/local\/v1\/plugins\/diagnostics/, 'smoke mode must check plugin diagnostics through the app');
|
|
77
|
+
});
|
|
@@ -9,6 +9,8 @@ const node_fs_1 = require("node:fs");
|
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
10
|
const node_os_1 = require("node:os");
|
|
11
11
|
const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-store.js");
|
|
12
|
+
const local_core_acp_session_coordinator_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-session-coordinator.js");
|
|
13
|
+
const scheduler_service_js_1 = require("../services/local-ai-core/src/scheduler/scheduler-service.js");
|
|
12
14
|
(0, node_test_1.default)('workspace registry entries persist in LocalCoreAcpStore', () => {
|
|
13
15
|
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'workspace-registry-'));
|
|
14
16
|
try {
|
|
@@ -34,6 +36,142 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
|
|
|
34
36
|
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
35
37
|
}
|
|
36
38
|
});
|
|
39
|
+
(0, node_test_1.default)('ACP runtime env includes the current workspace path for file returns', async () => {
|
|
40
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'workspace-env-'));
|
|
41
|
+
try {
|
|
42
|
+
const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
43
|
+
store.upsertWorkspaceRegistryEntry({
|
|
44
|
+
workspaceId: 'workspace-a',
|
|
45
|
+
displayName: 'Workspace A',
|
|
46
|
+
path: '/tmp/workspace-a',
|
|
47
|
+
deviceId: 'local',
|
|
48
|
+
defaultRuntimeId: 'opencode',
|
|
49
|
+
health: { status: 'healthy', summary: 'ok', issues: [] },
|
|
50
|
+
git: { isRepo: false },
|
|
51
|
+
});
|
|
52
|
+
const thread = store.createThread('workspace-a', 'Thread');
|
|
53
|
+
let capturedRuntimeEnv;
|
|
54
|
+
const coordinator = new local_core_acp_session_coordinator_js_1.LocalCoreAcpSessionCoordinator({
|
|
55
|
+
store,
|
|
56
|
+
transport: {
|
|
57
|
+
spawnSession(input) {
|
|
58
|
+
capturedRuntimeEnv = input.runtimeEnv;
|
|
59
|
+
return {
|
|
60
|
+
threadId: input.threadId,
|
|
61
|
+
bridgeSessionKey: input.bridgeSessionKey,
|
|
62
|
+
closed: false,
|
|
63
|
+
sessionId: '',
|
|
64
|
+
supportsLoad: false,
|
|
65
|
+
pendingPermissionByRun: new Map(),
|
|
66
|
+
schedulerJobCreatedByRun: new Map(),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
initializeSession: async () => { },
|
|
70
|
+
request: async () => ({ sessionId: 'session-1' }),
|
|
71
|
+
closeSession: () => { },
|
|
72
|
+
closeSessionWithError: () => { },
|
|
73
|
+
sendRaw: () => true,
|
|
74
|
+
},
|
|
75
|
+
runThreadMap: new Map(),
|
|
76
|
+
emitBridge: () => { },
|
|
77
|
+
});
|
|
78
|
+
await coordinator.ensureSession(thread.id, 'session:thread-1', {
|
|
79
|
+
workspaceId: 'workspace-a',
|
|
80
|
+
agentType: 'opencode',
|
|
81
|
+
command: 'opencode',
|
|
82
|
+
args: [],
|
|
83
|
+
env: {},
|
|
84
|
+
workDir: '/tmp/workspace-a',
|
|
85
|
+
model: '',
|
|
86
|
+
});
|
|
87
|
+
strict_1.default.equal(capturedRuntimeEnv?.LOCAL_AI_WORKSPACE_ID, 'workspace-a');
|
|
88
|
+
strict_1.default.equal(capturedRuntimeEnv?.LOCAL_AI_WORKSPACE_PATH, '/tmp/workspace-a');
|
|
89
|
+
store.close();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
(0, node_test_1.default)('scheduler uses short ids for new jobs and resolves legacy full ids by short id', () => {
|
|
96
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-id-store-'));
|
|
97
|
+
try {
|
|
98
|
+
const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
99
|
+
const scheduler = new scheduler_service_js_1.SchedulerService({
|
|
100
|
+
store,
|
|
101
|
+
triggers: [],
|
|
102
|
+
executors: [],
|
|
103
|
+
eventBus: { emit: () => { }, on: () => () => { } },
|
|
104
|
+
});
|
|
105
|
+
const created = scheduler.createJob({
|
|
106
|
+
workspaceId: 'workspace-a',
|
|
107
|
+
platform: 'local',
|
|
108
|
+
route: { type: 'local.thread', channelId: 'workspace-a', threadId: 'thread-1' },
|
|
109
|
+
executionMode: 'side-thread',
|
|
110
|
+
triggerType: 'cron',
|
|
111
|
+
cronExpr: '30 18 * * *',
|
|
112
|
+
promptTemplate: 'ping',
|
|
113
|
+
description: 'daily ping',
|
|
114
|
+
enabled: true,
|
|
115
|
+
});
|
|
116
|
+
strict_1.default.match(created.id, /^[0-9a-f]{8}$/);
|
|
117
|
+
const legacy = store.createScheduledJob({
|
|
118
|
+
workspaceId: 'workspace-a',
|
|
119
|
+
platform: 'local',
|
|
120
|
+
route: { type: 'local.thread', channelId: 'workspace-a', threadId: 'thread-1' },
|
|
121
|
+
executionMode: 'side-thread',
|
|
122
|
+
triggerType: 'cron',
|
|
123
|
+
cronExpr: '0 9 * * *',
|
|
124
|
+
promptTemplate: 'legacy',
|
|
125
|
+
description: 'legacy job',
|
|
126
|
+
enabled: true,
|
|
127
|
+
});
|
|
128
|
+
store.db.prepare('UPDATE scheduled_jobs SET id = ? WHERE id = ?').run('job:826aff79-570b-4308-822e-18318e2c96ba', legacy.id);
|
|
129
|
+
strict_1.default.equal(scheduler.getJob('826aff79')?.id, 'job:826aff79-570b-4308-822e-18318e2c96ba');
|
|
130
|
+
scheduler.updateJob('826aff79', { description: 'updated legacy job' });
|
|
131
|
+
strict_1.default.equal(scheduler.getJob('826aff79')?.description, 'updated legacy job');
|
|
132
|
+
scheduler.deleteJob('826aff79');
|
|
133
|
+
strict_1.default.equal(scheduler.getJob('826aff79'), undefined);
|
|
134
|
+
strict_1.default.throws(() => scheduler.deleteJob('826aff79'), /Scheduled job not found: 826aff79/);
|
|
135
|
+
store.close();
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
(0, node_test_1.default)('scheduled jobs normalize enum-like input before persistence', () => {
|
|
142
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-enum-store-'));
|
|
143
|
+
try {
|
|
144
|
+
const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
145
|
+
const created = store.createScheduledJob({
|
|
146
|
+
workspaceId: 'workspace-a',
|
|
147
|
+
platform: ' Lark ',
|
|
148
|
+
route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
|
|
149
|
+
executionMode: 'side_thread',
|
|
150
|
+
triggerType: 'CRON',
|
|
151
|
+
cronExpr: '30 18 * * *',
|
|
152
|
+
promptTemplate: 'ping',
|
|
153
|
+
description: 'daily ping',
|
|
154
|
+
enabled: true,
|
|
155
|
+
});
|
|
156
|
+
strict_1.default.equal(created.platform, 'lark');
|
|
157
|
+
strict_1.default.equal(created.executionMode, 'side-thread');
|
|
158
|
+
strict_1.default.equal(created.triggerType, 'cron');
|
|
159
|
+
const updated = store.updateScheduledJob(created.id, {
|
|
160
|
+
executionMode: 'same_thread',
|
|
161
|
+
triggerType: 'one time',
|
|
162
|
+
runAt: '2026-05-04T10:00:00.000Z',
|
|
163
|
+
cronExpr: '',
|
|
164
|
+
});
|
|
165
|
+
strict_1.default.equal(updated.executionMode, 'same-thread');
|
|
166
|
+
strict_1.default.equal(updated.triggerType, 'once');
|
|
167
|
+
strict_1.default.equal(updated.runAt, '2026-05-04T10:00:00.000Z');
|
|
168
|
+
strict_1.default.equal(updated.cronExpr, undefined);
|
|
169
|
+
store.close();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
37
175
|
(0, node_test_1.default)('agent tasks persist, update status, and can be found by run id', () => {
|
|
38
176
|
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agent-task-store-'));
|
|
39
177
|
try {
|
|
@@ -64,3 +202,46 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
|
|
|
64
202
|
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
65
203
|
}
|
|
66
204
|
});
|
|
205
|
+
(0, node_test_1.default)('agent task and run statuses normalize before persistence', () => {
|
|
206
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'task-run-status-store-'));
|
|
207
|
+
try {
|
|
208
|
+
const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
209
|
+
const task = store.createAgentTask({
|
|
210
|
+
workspaceId: 'workspace-a',
|
|
211
|
+
deviceId: 'device-a',
|
|
212
|
+
runtimeId: 'runtime-a',
|
|
213
|
+
title: 'Normalize states',
|
|
214
|
+
status: 'waiting for user',
|
|
215
|
+
});
|
|
216
|
+
strict_1.default.equal(task.status, 'waiting_for_user');
|
|
217
|
+
strict_1.default.equal(store.listAgentTasks({ status: 'waiting for user' }).tasks.length, 1);
|
|
218
|
+
const updated = store.updateAgentTask(task.taskId, { status: 'canceled' });
|
|
219
|
+
strict_1.default.equal(updated.status, 'cancelled');
|
|
220
|
+
strict_1.default.equal(updated.timeline.at(-1)?.status, 'cancelled');
|
|
221
|
+
const thread = store.createThread('workspace-a', 'Thread');
|
|
222
|
+
store.updateRun('run-1', thread.id, 'awaiting input');
|
|
223
|
+
strict_1.default.equal(store.getRun('run-1')?.status, 'awaiting_input');
|
|
224
|
+
store.updateRun('run-1', thread.id, 'canceled');
|
|
225
|
+
strict_1.default.equal(store.getRun('run-1')?.status, 'interrupted');
|
|
226
|
+
const job = store.createScheduledJob({
|
|
227
|
+
workspaceId: 'workspace-a',
|
|
228
|
+
platform: 'local',
|
|
229
|
+
route: { type: 'local.thread', channelId: 'workspace-a', threadId: 'thread-1' },
|
|
230
|
+
executionMode: 'same-thread',
|
|
231
|
+
triggerType: 'cron',
|
|
232
|
+
cronExpr: '0 9 * * *',
|
|
233
|
+
promptTemplate: 'ping',
|
|
234
|
+
description: 'daily ping',
|
|
235
|
+
enabled: true,
|
|
236
|
+
});
|
|
237
|
+
const run = store.createScheduledJobRun(job.id, 'complete');
|
|
238
|
+
strict_1.default.equal(run.status, 'succeeded');
|
|
239
|
+
const skipped = store.updateScheduledJobRun(run.id, { status: 'cancelled' });
|
|
240
|
+
strict_1.default.equal(skipped.status, 'skipped');
|
|
241
|
+
strict_1.default.equal(store.getScheduledJob(job.id)?.lastStatus, 'skipped');
|
|
242
|
+
store.close();
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
@@ -1,2 +1,112 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeApprovalRequestStatus = normalizeApprovalRequestStatus;
|
|
4
|
+
exports.normalizeAgentTaskStatus = normalizeAgentTaskStatus;
|
|
5
|
+
exports.normalizeChannelContentPartType = normalizeChannelContentPartType;
|
|
6
|
+
exports.normalizeScheduledJobExecutionMode = normalizeScheduledJobExecutionMode;
|
|
7
|
+
exports.normalizeScheduledJobTriggerType = normalizeScheduledJobTriggerType;
|
|
8
|
+
exports.normalizeChannelPlatform = normalizeChannelPlatform;
|
|
9
|
+
exports.normalizeRunStatus = normalizeRunStatus;
|
|
10
|
+
exports.normalizeScheduledJobRunStatus = normalizeScheduledJobRunStatus;
|
|
11
|
+
function normalizeApprovalRequestStatus(value, fallback = 'pending') {
|
|
12
|
+
const normalized = normalizeContractEnumValue(value || fallback).replace(/-/g, '_');
|
|
13
|
+
if (normalized === 'pending' ||
|
|
14
|
+
normalized === 'approved' ||
|
|
15
|
+
normalized === 'rejected' ||
|
|
16
|
+
normalized === 'cancelled' ||
|
|
17
|
+
normalized === 'expired') {
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
if (normalized === 'approve') {
|
|
21
|
+
return 'approved';
|
|
22
|
+
}
|
|
23
|
+
if (normalized === 'reject') {
|
|
24
|
+
return 'rejected';
|
|
25
|
+
}
|
|
26
|
+
if (normalized === 'canceled') {
|
|
27
|
+
return 'cancelled';
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Approval request status must be pending, approved, rejected, cancelled, or expired.');
|
|
30
|
+
}
|
|
31
|
+
function normalizeAgentTaskStatus(value, fallback = 'created') {
|
|
32
|
+
const normalized = normalizeContractEnumValue(value || fallback).replace(/-/g, '_');
|
|
33
|
+
if (normalized === 'created' ||
|
|
34
|
+
normalized === 'queued' ||
|
|
35
|
+
normalized === 'running' ||
|
|
36
|
+
normalized === 'waiting_for_user' ||
|
|
37
|
+
normalized === 'completed' ||
|
|
38
|
+
normalized === 'failed' ||
|
|
39
|
+
normalized === 'cancelled') {
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
if (normalized === 'canceled') {
|
|
43
|
+
return 'cancelled';
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Agent task status must be created, queued, running, waiting_for_user, completed, failed, or cancelled.');
|
|
46
|
+
}
|
|
47
|
+
function normalizeChannelContentPartType(value) {
|
|
48
|
+
const normalized = normalizeContractEnumValue(value).replace(/-/g, '_');
|
|
49
|
+
if (normalized === 'text' ||
|
|
50
|
+
normalized === 'image' ||
|
|
51
|
+
normalized === 'file' ||
|
|
52
|
+
normalized === 'permission_card') {
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
throw new Error('Channel content part type must be text, image, file, or permission_card.');
|
|
56
|
+
}
|
|
57
|
+
function normalizeContractEnumValue(value) {
|
|
58
|
+
return String(value || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
59
|
+
}
|
|
60
|
+
function normalizeScheduledJobExecutionMode(value, fallback = 'same-thread') {
|
|
61
|
+
const normalized = normalizeContractEnumValue(value || fallback);
|
|
62
|
+
if (normalized === 'same-thread' || normalized === 'side-thread') {
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Scheduled job execution mode must be same-thread or side-thread.');
|
|
66
|
+
}
|
|
67
|
+
function normalizeScheduledJobTriggerType(value, fallback = 'cron') {
|
|
68
|
+
const normalized = normalizeContractEnumValue(value || fallback);
|
|
69
|
+
if (normalized === 'cron' || normalized === 'once' || normalized === 'one-time') {
|
|
70
|
+
return normalized === 'one-time' ? 'once' : normalized;
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Scheduled job trigger type must be cron or once.');
|
|
73
|
+
}
|
|
74
|
+
function normalizeChannelPlatform(value) {
|
|
75
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
throw new Error('Channel platform is required.');
|
|
78
|
+
}
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
function normalizeRunStatus(value, fallback = 'queued') {
|
|
82
|
+
const normalized = normalizeContractEnumValue(value || fallback).replace(/-/g, '_');
|
|
83
|
+
if (normalized === 'queued' ||
|
|
84
|
+
normalized === 'running' ||
|
|
85
|
+
normalized === 'awaiting_input' ||
|
|
86
|
+
normalized === 'completed' ||
|
|
87
|
+
normalized === 'failed' ||
|
|
88
|
+
normalized === 'interrupted') {
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
if (normalized === 'cancelled' || normalized === 'canceled') {
|
|
92
|
+
return 'interrupted';
|
|
93
|
+
}
|
|
94
|
+
throw new Error('Run status must be queued, running, awaiting_input, completed, failed, or interrupted.');
|
|
95
|
+
}
|
|
96
|
+
function normalizeScheduledJobRunStatus(value, fallback = 'queued') {
|
|
97
|
+
const normalized = normalizeContractEnumValue(value || fallback).replace(/-/g, '_');
|
|
98
|
+
if (normalized === 'queued' ||
|
|
99
|
+
normalized === 'running' ||
|
|
100
|
+
normalized === 'succeeded' ||
|
|
101
|
+
normalized === 'failed' ||
|
|
102
|
+
normalized === 'skipped') {
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
if (normalized === 'complete' || normalized === 'completed' || normalized === 'success') {
|
|
106
|
+
return 'succeeded';
|
|
107
|
+
}
|
|
108
|
+
if (normalized === 'cancelled' || normalized === 'canceled') {
|
|
109
|
+
return 'skipped';
|
|
110
|
+
}
|
|
111
|
+
throw new Error('Scheduled job run status must be queued, running, succeeded, failed, or skipped.');
|
|
112
|
+
}
|
|
@@ -9,6 +9,7 @@ const local_core_acp_session_coordinator_js_1 = require("./local-core-acp-sessio
|
|
|
9
9
|
const local_core_acp_response_processor_js_1 = require("./local-core-acp-response-processor.js");
|
|
10
10
|
const local_core_acp_content_js_1 = require("./local-core-acp-content.js");
|
|
11
11
|
const command_risk_js_1 = require("../security/command-risk.js");
|
|
12
|
+
const local_core_slash_commands_js_1 = require("./local-core-slash-commands.js");
|
|
12
13
|
const ACP_PROMPT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
13
14
|
class LocalCoreAcpBackend {
|
|
14
15
|
options;
|
|
@@ -26,11 +27,11 @@ class LocalCoreAcpBackend {
|
|
|
26
27
|
});
|
|
27
28
|
this.turnCoordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
|
|
28
29
|
emitBridge: (event) => this.emitBridgeEvent(event),
|
|
29
|
-
appendMessage: (threadId, role, content, kind) => {
|
|
30
|
-
this.options.store.appendMessage(threadId, role, content, kind);
|
|
30
|
+
appendMessage: (threadId, role, content, kind, toolCall) => {
|
|
31
|
+
this.options.store.appendMessage(threadId, role, content, kind, toolCall);
|
|
31
32
|
},
|
|
32
|
-
upsertMessage: (threadId, id, role, content, kind) => {
|
|
33
|
-
this.options.store.upsertMessage(threadId, id, role, content, kind);
|
|
33
|
+
upsertMessage: (threadId, id, role, content, kind, toolCall) => {
|
|
34
|
+
this.options.store.upsertMessage(threadId, id, role, content, kind, toolCall);
|
|
34
35
|
},
|
|
35
36
|
updateRunStatus: (runId, threadId, status) => {
|
|
36
37
|
this.options.store.updateRun(runId, threadId, status);
|
|
@@ -71,6 +72,7 @@ class LocalCoreAcpBackend {
|
|
|
71
72
|
});
|
|
72
73
|
return approval.approvalId;
|
|
73
74
|
},
|
|
75
|
+
getThreadAgentMode: (threadId) => this.options.store.getThreadRow(threadId)?.agent_mode || local_core_slash_commands_js_1.DEFAULT_AGENT_MODE,
|
|
74
76
|
sendRaw: (session, payload) => this.transport.sendRaw(session, payload),
|
|
75
77
|
});
|
|
76
78
|
this.sessionCoordinator = new local_core_acp_session_coordinator_js_1.LocalCoreAcpSessionCoordinator({
|
|
@@ -108,8 +110,8 @@ class LocalCoreAcpBackend {
|
|
|
108
110
|
async listThreads(workspaceId) {
|
|
109
111
|
return this.options.store.listThreadSummaries(workspaceId);
|
|
110
112
|
}
|
|
111
|
-
async createThread(workspaceId, title, agentType = desktop_js_1.LOCALCORE_ACP_AGENT_TYPE) {
|
|
112
|
-
return this.options.store.createThread(workspaceId, title, agentType);
|
|
113
|
+
async createThread(workspaceId, title, agentType = desktop_js_1.LOCALCORE_ACP_AGENT_TYPE, agentMode = local_core_slash_commands_js_1.DEFAULT_AGENT_MODE) {
|
|
114
|
+
return this.options.store.createThread(workspaceId, title, agentType, agentMode);
|
|
113
115
|
}
|
|
114
116
|
async getThread(threadId) {
|
|
115
117
|
const detail = this.options.store.getThread(threadId, []);
|
|
@@ -149,6 +151,31 @@ class LocalCoreAcpBackend {
|
|
|
149
151
|
source: 'user',
|
|
150
152
|
},
|
|
151
153
|
});
|
|
154
|
+
const slashResponse = await this.handleLocalSlashCommand(threadId, row.workspace_id, content);
|
|
155
|
+
if (slashResponse) {
|
|
156
|
+
this.options.store.appendMessage(threadId, 'assistant', slashResponse, 'final');
|
|
157
|
+
this.options.eventBus.emit({
|
|
158
|
+
type: 'thread.message.accepted',
|
|
159
|
+
payload: {
|
|
160
|
+
threadId,
|
|
161
|
+
workspaceId: row.workspace_id,
|
|
162
|
+
role: 'assistant',
|
|
163
|
+
content: slashResponse,
|
|
164
|
+
kind: 'final',
|
|
165
|
+
source: 'system',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
this.emitBridgeEvent({
|
|
169
|
+
type: 'reply',
|
|
170
|
+
sessionKey: row.bridge_session_key,
|
|
171
|
+
content: slashResponse,
|
|
172
|
+
});
|
|
173
|
+
this.emitBridgeEvent({
|
|
174
|
+
type: 'typing_stop',
|
|
175
|
+
sessionKey: row.bridge_session_key,
|
|
176
|
+
});
|
|
177
|
+
return { runId: '' };
|
|
178
|
+
}
|
|
152
179
|
const runId = `run:${threadId}:${Date.now()}`;
|
|
153
180
|
this.options.runThreadMap.set(runId, threadId);
|
|
154
181
|
this.options.store.updateRun(runId, threadId, 'running');
|
|
@@ -219,6 +246,40 @@ class LocalCoreAcpBackend {
|
|
|
219
246
|
});
|
|
220
247
|
return { runId: session.currentRunId };
|
|
221
248
|
}
|
|
249
|
+
async handleLocalSlashCommand(threadId, workspaceId, content) {
|
|
250
|
+
const command = (0, local_core_slash_commands_js_1.parseSlashCommand)(content);
|
|
251
|
+
if (!command || command.name !== 'mode') {
|
|
252
|
+
return '';
|
|
253
|
+
}
|
|
254
|
+
const row = this.options.store.getThreadRow(threadId);
|
|
255
|
+
const currentMode = row?.agent_mode || local_core_slash_commands_js_1.DEFAULT_AGENT_MODE;
|
|
256
|
+
const requested = command.args.join(' ').trim();
|
|
257
|
+
if (!requested) {
|
|
258
|
+
return (0, local_core_slash_commands_js_1.modeHelpText)(currentMode);
|
|
259
|
+
}
|
|
260
|
+
const mode = (0, local_core_slash_commands_js_1.normalizeAgentMode)(requested);
|
|
261
|
+
if (!mode) {
|
|
262
|
+
return `未知模式:${requested}\n\n${(0, local_core_slash_commands_js_1.modeHelpText)(currentMode)}`;
|
|
263
|
+
}
|
|
264
|
+
this.options.store.updateThreadAgentMode(threadId, mode);
|
|
265
|
+
try {
|
|
266
|
+
await this.sessionCoordinator.setThreadMode(threadId, mode);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
this.options.log?.(`localcore-acp mode sync failed for ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
270
|
+
}
|
|
271
|
+
this.options.store.createAuditEvent({
|
|
272
|
+
type: 'permission.changed',
|
|
273
|
+
workspaceId,
|
|
274
|
+
actor: 'local',
|
|
275
|
+
summary: `Thread agent mode changed to ${mode}.`,
|
|
276
|
+
metadata: { threadId, mode },
|
|
277
|
+
});
|
|
278
|
+
if (mode === 'bypassPermissions') {
|
|
279
|
+
return '已切换到 yolo 模式:后续工具调用会跳过权限申请。使用 `/mode default` 可恢复默认权限。';
|
|
280
|
+
}
|
|
281
|
+
return `已切换到 ${(0, local_core_slash_commands_js_1.formatAgentMode)(mode)} 模式。`;
|
|
282
|
+
}
|
|
222
283
|
async interruptRun(runId) {
|
|
223
284
|
return this.sessionCoordinator.interruptRun(runId);
|
|
224
285
|
}
|