@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.
Files changed (93) hide show
  1. package/README.md +3 -0
  2. package/dist/renderer/assets/{Badge-C2K5apfV.js → Badge-Bn2h-Qkf.js} +1 -1
  3. package/dist/renderer/assets/{Chat-DI2YeVKb.js → Chat-Cgo9aneg.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-DMAr15Oi.js → ChatMarkdown-CYzHfSiR.js} +1 -1
  5. package/dist/renderer/assets/{Config-7rP3EuUf.js → Config-DljB2-eP.js} +1 -1
  6. package/dist/renderer/assets/{CronList-81QslYE8.js → CronList-DeVbiY9a.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-Db2T7rSZ.js → Dashboard-fSqx5sm8.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-BBJ3cTvB.js → EmptyState-DgqKDwlq.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-Dz_6flG3.js → KnowledgeDetail-u8s2SmPV.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-DIh046mN.js → KnowledgeHome-BdQw8nbY.js} +1 -1
  11. package/dist/renderer/assets/{Logs-BXBtHJLf.js → Logs-DSbndMpy.js} +1 -1
  12. package/dist/renderer/assets/{Modal-BB2Av_Xs.js → Modal-0d0EmAuK.js} +1 -1
  13. package/dist/renderer/assets/{Page-DPSdvMgU.js → Page-CVbOH_Kh.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-C6EWGvGg.js → ProjectDetail-C91tF7uA.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-BBo5ygnS.js → ProjectList-D78Pc-BQ.js} +1 -1
  16. package/dist/renderer/assets/{Select-CNX-Tx_O.js → Select-BbKzAIuO.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-BIn46jqZ.js → SessionChat-BERvL5e4.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-CZt6f1Rd.js → SessionList-Bomz335M.js} +1 -1
  19. package/dist/renderer/assets/ThreadChat-CLr3amU2.js +23 -0
  20. package/dist/renderer/assets/{Workspace-C_4tdpjX.js → Workspace-In91z4W2.js} +1 -1
  21. package/dist/renderer/assets/{arrow-left-cHr5wkpq.js → arrow-left-FBnVKKBl.js} +1 -1
  22. package/dist/renderer/assets/{book-open-4nx5C8T-.js → book-open-4_CuP6Sa.js} +1 -1
  23. package/dist/renderer/assets/{check-exTS-R-x.js → check-vsoVjvac.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-DSvxXQa0.js → chevron-down-DFpmFTJW.js} +1 -1
  25. package/dist/renderer/assets/{circle-DyvEovdw.js → circle-vRULegER.js} +1 -1
  26. package/dist/renderer/assets/desktop-BkMW-h5p.js +4 -0
  27. package/dist/renderer/assets/{desktop-B-KVT10D.js → desktop-ul0Tpzyc.js} +1 -1
  28. package/dist/renderer/assets/{funnel-DA9qVdTy.js → funnel-Gu0E_hJ_.js} +1 -1
  29. package/dist/renderer/assets/{heart-DuEV0ZRJ.js → heart-DsdofOHf.js} +1 -1
  30. package/dist/renderer/assets/{index-Biz-aexG.js → index-BD3aCyBa.js} +3 -3
  31. package/dist/renderer/assets/{index-BgflqkCN.js → index-pJUFobU7.js} +1 -1
  32. package/dist/renderer/assets/{pencil-DBDxeNmO.js → pencil-DbKmV8nD.js} +1 -1
  33. package/dist/renderer/assets/{play-IzlP83_7.js → play-BDMsRxSm.js} +1 -1
  34. package/dist/renderer/assets/{plug-CuT-qFnW.js → plug-t2dMl78-.js} +1 -1
  35. package/dist/renderer/assets/{plus-DEqgkn63.js → plus-BUb6zOAe.js} +1 -1
  36. package/dist/renderer/assets/projects-4F6D_Q8t.js +1 -0
  37. package/dist/renderer/assets/{save-D4sRo07y.js → save-C-_5rQdv.js} +1 -1
  38. package/dist/renderer/assets/{search-DbPZLuV8.js → search-R3ekHFPM.js} +1 -1
  39. package/dist/renderer/assets/{server-D1jUQmwN.js → server-BTuvHf5n.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-Aife8ep2.js → session-utils-ClX6VPFG.js} +1 -1
  41. package/dist/renderer/assets/{sessions-DaH3H7gC.js → sessions-AFKIC-ub.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-BGwUFWNr.js → shield-check-CqMW8CHe.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-CIFBioBa.js → trash-2-BNLcHyUw.js} +1 -1
  44. package/dist/renderer/assets/{user-CnKcvHdo.js → user-BCCN8FYK.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-ClO53WGa.js → wifi-off-BsNsx3oX.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/electron/agent-runtime-detector.test.js +64 -0
  48. package/dist-electron/electron/architecture-docs.test.js +56 -0
  49. package/dist-electron/electron/channel-content.test.js +37 -0
  50. package/dist-electron/electron/lac-cli.test.js +297 -4
  51. package/dist-electron/electron/local-core-acp-content.test.js +39 -0
  52. package/dist-electron/electron/local-core-contracts.test.js +67 -0
  53. package/dist-electron/electron/local-core-refactor.test.js +789 -3
  54. package/dist-electron/electron/plugin-kernel.test.js +108 -0
  55. package/dist-electron/electron/renderer-packaging.test.js +77 -0
  56. package/dist-electron/electron/workspace-task-store.test.js +181 -0
  57. package/dist-electron/packages/contracts/src/local-core.js +110 -0
  58. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +67 -6
  59. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +25 -4
  60. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-permission-lifecycle.js +76 -0
  61. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-progress.js +201 -0
  62. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +3 -2
  63. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +29 -0
  64. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +74 -46
  65. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +112 -210
  66. package/dist-electron/services/local-ai-core/src/acp/local-core-slash-commands.js +60 -0
  67. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +5 -48
  68. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +83 -28
  69. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +11 -0
  70. package/dist-electron/services/local-ai-core/src/channel/shared/content.js +23 -0
  71. package/dist-electron/services/local-ai-core/src/channel/shared/file-utils.js +13 -1
  72. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +31 -29
  73. package/dist-electron/services/local-ai-core/src/cli/lac.js +22 -17
  74. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +1 -0
  75. package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +9 -0
  76. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +221 -10
  77. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +39 -9
  78. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +1 -0
  79. package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-service.js +1 -0
  80. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +439 -0
  81. package/dist-electron/services/local-ai-core/src/runtime/server.js +208 -303
  82. package/dist-electron/services/local-ai-core/src/scheduler/job-id.js +15 -0
  83. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +30 -5
  84. package/dist-electron/shared/desktop.js +34 -8
  85. package/dist-electron/src/pages/Threads/thread-chat-message-blocks.js +187 -0
  86. package/dist-electron/src/pages/Threads/thread-chat-model.js +58 -0
  87. package/dist-electron/src/pages/Threads/thread-chat-page-state.js +96 -0
  88. package/dist-electron/src/pages/Threads/thread-chat-permission.js +18 -0
  89. package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +347 -0
  90. package/package.json +4 -2
  91. package/dist/renderer/assets/ThreadChat-BotD0gKQ.js +0 -23
  92. package/dist/renderer/assets/desktop-CVibRyAw.js +0 -4
  93. 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
  }