@kafca/agentdock 0.1.44 → 0.1.46

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 (126) hide show
  1. package/README.md +76 -11
  2. package/bin/agentdock.mjs +5 -5
  3. package/dist/renderer/assets/{Badge-Qk9qHovH.js → Badge-BIesaDYG.js} +1 -1
  4. package/dist/renderer/assets/Chat-CpataoDh.js +2 -0
  5. package/dist/renderer/assets/{ChatMarkdown-BKdW8OON.js → ChatMarkdown-_Mdmltgv.js} +1 -1
  6. package/dist/renderer/assets/Config-Pe5-zKZk.js +21 -0
  7. package/dist/renderer/assets/CronList-BnaOSPT8.js +1 -0
  8. package/dist/renderer/assets/Dashboard-D05l8GKE.js +26 -0
  9. package/dist/renderer/assets/{EmptyState-kD9yAwtw.js → EmptyState-BDBavDll.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeDetail-BAMCLrjz.js → KnowledgeDetail-DvR798l_.js} +2 -2
  11. package/dist/renderer/assets/KnowledgeHome-r2ZuvU8-.js +11 -0
  12. package/dist/renderer/assets/Logs-BaljtmAV.js +1 -0
  13. package/dist/renderer/assets/{Modal-DwY4lecS.js → Modal-CXhD_OWR.js} +1 -1
  14. package/dist/renderer/assets/MonitorList-D7tyipbx.js +1 -0
  15. package/dist/renderer/assets/{Page-CnB9ePsb.js → Page-KUuFYgjg.js} +1 -1
  16. package/dist/renderer/assets/ProjectDetail-Ddq94-yu.js +11 -0
  17. package/dist/renderer/assets/ProjectList-CbdHd5Ev.js +1 -0
  18. package/dist/renderer/assets/{Select-zfoksLGB.js → Select-Djc8BXMC.js} +1 -1
  19. package/dist/renderer/assets/SessionChat-DOy0iKQR.js +1 -0
  20. package/dist/renderer/assets/SessionList-DHgl1s1D.js +1 -0
  21. package/dist/renderer/assets/ThreadChat-I7dYibDc.js +23 -0
  22. package/dist/renderer/assets/Workspace-DTocqY-F.js +19 -0
  23. package/dist/renderer/assets/{arrow-left-CNEAuElg.js → arrow-left-CXyya3hP.js} +1 -1
  24. package/dist/renderer/assets/{book-open-BNJMhNI-.js → book-open-9n4Kxc7E.js} +1 -1
  25. package/dist/renderer/assets/bot-C9HBk5XH.js +6 -0
  26. package/dist/renderer/assets/{check-BnclClRU.js → check-CutSg9a5.js} +1 -1
  27. package/dist/renderer/assets/{chevron-down-Bh53e0O-.js → chevron-down-DrydR4i3.js} +1 -1
  28. package/dist/renderer/assets/{circle-ClZTWCWO.js → circle-BEjtahw6.js} +1 -1
  29. package/dist/renderer/assets/desktop-DATgk_ka.js +1 -0
  30. package/dist/renderer/assets/{desktop-C6q6wBae.js → desktop-JQkhNOF4.js} +5 -5
  31. package/dist/renderer/assets/en-CmAr_0e6.js +1 -0
  32. package/dist/renderer/assets/es-Cs9x52ca.js +1 -0
  33. package/dist/renderer/assets/{funnel-_5hsz0xI.js → funnel-BhqZpS4f.js} +1 -1
  34. package/dist/renderer/assets/{heart-u4Q1aOqj.js → heart-Dp8Vs-7_.js} +1 -1
  35. package/dist/renderer/assets/{index-BNFx4uYv.js → index-BKpv0P0s.js} +2 -2
  36. package/dist/renderer/assets/index-CO4fGqmv.css +11 -0
  37. package/dist/renderer/assets/{index-B36eowj-.js → index-cI3pWBnN.js} +36 -36
  38. package/dist/renderer/assets/ja-DtvtBRTW.js +1 -0
  39. package/dist/renderer/assets/{pencil-DLh9QNmT.js → pencil-ywcJzeOo.js} +1 -1
  40. package/dist/renderer/assets/{play-BDTk9Scm.js → play-AtN492Cx.js} +1 -1
  41. package/dist/renderer/assets/{plus-BSc8POsz.js → plus-BEj14BBV.js} +1 -1
  42. package/dist/renderer/assets/projects-Clar8lgb.js +1 -0
  43. package/dist/renderer/assets/save-D6ov4CNv.js +11 -0
  44. package/dist/renderer/assets/{search-CniPk7Ey.js → search-C3bvw7AR.js} +1 -1
  45. package/dist/renderer/assets/{server-GsXyQAZm.js → server-C1Uyedyx.js} +1 -1
  46. package/dist/renderer/assets/{session-utils-BDxzgTBC.js → session-utils-DpRwJ6Nh.js} +1 -1
  47. package/dist/renderer/assets/{sessions-CLcKA1Tt.js → sessions-BWmAppwK.js} +1 -1
  48. package/dist/renderer/assets/{shield-check-4i0E3hmf.js → shield-check-CVx1mEs2.js} +1 -1
  49. package/dist/renderer/assets/{trash-2-DhtOCsO5.js → trash-2-o-heXOgs.js} +1 -1
  50. package/dist/renderer/assets/user-dPOzUqIc.js +6 -0
  51. package/dist/renderer/assets/{wifi-off-BHPzPY_B.js → wifi-off-Fz5c3_x_.js} +1 -1
  52. package/dist/renderer/assets/zh-CcJ9vBZj.js +1 -0
  53. package/dist/renderer/assets/zh-TW-elI72AC1.js +1 -0
  54. package/dist/renderer/index.html +2 -2
  55. package/dist-electron/electron/main.js +26 -1
  56. package/dist-electron/packages/core-sdk/src/index.js +28 -0
  57. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +111 -62
  58. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +80 -7
  59. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +78 -5
  60. package/dist-electron/services/local-ai-core/src/acp/store/external-store.js +98 -0
  61. package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +33 -0
  62. package/dist-electron/services/local-ai-core/src/acp/store/model-provider-store.js +88 -0
  63. package/dist-electron/services/local-ai-core/src/acp/store/schema.js +43 -0
  64. package/dist-electron/services/local-ai-core/src/agents/pi/launch.js +2 -119
  65. package/dist-electron/services/local-ai-core/src/agents/pi/provider-materializer.js +148 -0
  66. package/dist-electron/services/local-ai-core/src/channel/lark/inbound.js +55 -1
  67. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +65 -10
  68. package/dist-electron/services/local-ai-core/src/channel/shared/session-command-runtime.js +4 -2
  69. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +139 -29
  70. package/dist-electron/services/local-ai-core/src/execution/agent-execution-backend.js +56 -0
  71. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +18 -57
  72. package/dist-electron/services/local-ai-core/src/kernel/local-core-errors.js +255 -0
  73. package/dist-electron/services/local-ai-core/src/plugins/builtin/catalog.js +78 -0
  74. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +3 -1
  75. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +64 -9
  76. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +5 -2
  77. package/dist-electron/services/local-ai-core/src/runtime/config-migration.js +152 -0
  78. package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +175 -0
  79. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +330 -3
  80. package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +11 -2
  81. package/dist-electron/services/local-ai-core/src/runtime/provider-config-migration.js +32 -0
  82. package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-service.js +76 -8
  83. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +62 -0
  84. package/dist-electron/services/local-ai-core/src/runtime/server.js +708 -4
  85. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +13 -3
  86. package/dist-electron/services/local-ai-core/src/sandbox/opensandbox-client.js +113 -0
  87. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-config.js +273 -0
  88. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-manager.js +215 -0
  89. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-stdio-proxy.js +188 -0
  90. package/dist-electron/services/local-ai-core/src/thread/slash-command-registry.js +3 -0
  91. package/dist-electron/services/local-ai-core/src/thread/thread-command-service.js +32 -0
  92. package/dist-electron/services/local-ai-core/src/thread/thread-slash-command-dispatcher.js +71 -0
  93. package/dist-electron/shared/desktop.js +85 -1
  94. package/dist-electron/tests/contracts/architecture-docs.test.js +83 -0
  95. package/dist-electron/tests/electron/plugin-kernel.test.js +225 -18
  96. package/dist-electron/tests/electron/runtime-detection-service.test.js +57 -5
  97. package/dist-electron/tests/electron/sandbox-config.test.js +541 -0
  98. package/dist-electron/tests/electron/workspace-task-store.test.js +257 -0
  99. package/dist-electron/tests/integration/lark-gateway.test.js +139 -0
  100. package/dist-electron/tests/integration/local-core-refactor.test.js +183 -20
  101. package/dist-electron/tests/integration/local-core-routes.test.js +34 -0
  102. package/package.json +2 -3
  103. package/dist/renderer/assets/Chat-BK-fm0Xr.js +0 -2
  104. package/dist/renderer/assets/Config-B93CQGvY.js +0 -16
  105. package/dist/renderer/assets/CronList-Z3TvSRKP.js +0 -1
  106. package/dist/renderer/assets/Dashboard-Ch3QkWwf.js +0 -26
  107. package/dist/renderer/assets/KnowledgeHome-ChvjOfrc.js +0 -11
  108. package/dist/renderer/assets/Logs-Cv0nth3j.js +0 -1
  109. package/dist/renderer/assets/MonitorList-JEvMoWUm.js +0 -1
  110. package/dist/renderer/assets/ProjectDetail-DJa3sMmU.js +0 -11
  111. package/dist/renderer/assets/ProjectList-D6CXumR_.js +0 -1
  112. package/dist/renderer/assets/SessionChat-DUTh4WoM.js +0 -1
  113. package/dist/renderer/assets/SessionList-D9o-lUst.js +0 -1
  114. package/dist/renderer/assets/ThreadChat-BYXlT-Kc.js +0 -23
  115. package/dist/renderer/assets/Workspace-BrhSwRQ-.js +0 -14
  116. package/dist/renderer/assets/desktop-C5vBT0yM.js +0 -1
  117. package/dist/renderer/assets/en-D2SkCrhG.js +0 -1
  118. package/dist/renderer/assets/es-C8UBPhxM.js +0 -1
  119. package/dist/renderer/assets/index-CaQZSRbg.css +0 -11
  120. package/dist/renderer/assets/ja-ChF4vbBL.js +0 -1
  121. package/dist/renderer/assets/plug-BG0JHW6B.js +0 -6
  122. package/dist/renderer/assets/projects-DwfZwxDW.js +0 -1
  123. package/dist/renderer/assets/save-D5AO_G6a.js +0 -6
  124. package/dist/renderer/assets/user-BtC_pVXV.js +0 -11
  125. package/dist/renderer/assets/zh-DIcdHycM.js +0 -1
  126. package/dist/renderer/assets/zh-TW-BtRwreY_.js +0 -1
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDeploymentDiagnostics = runDeploymentDiagnostics;
4
+ const node_fs_1 = require("node:fs");
5
+ const index_js_1 = require("../../../../packages/contracts/src/index.js");
6
+ const sandbox_config_js_1 = require("../sandbox/sandbox-config.js");
7
+ async function runDeploymentDiagnostics(input) {
8
+ const env = input.env || process.env;
9
+ const checkedAt = new Date().toISOString();
10
+ const config = input.config || {};
11
+ const profile = (0, index_js_1.getDesktopDeploymentProfile)(String(config.deployment_profile || env.AGENTDOCK_DEPLOYMENT_PROFILE || '').trim());
12
+ const provider = Array.isArray(config.sandbox_providers)
13
+ ? config.sandbox_providers.find((item) => item.id === profile.defaultSandboxProviderId) || config.sandbox_providers[0]
14
+ : undefined;
15
+ const sandboxProvider = provider || (0, index_js_1.defaultSandboxProviderForProfile)(profile.id);
16
+ const opensandboxUrl = normalizeUrl(sandboxProvider.server_url || env.AGENTDOCK_OPENSANDBOX_SERVER_URL || (0, sandbox_config_js_1.defaultOpenSandboxServerUrl)(env));
17
+ const checks = [
18
+ {
19
+ id: 'deployment.profile',
20
+ label: 'Deployment profile',
21
+ status: 'pass',
22
+ summary: `${profile.label} uses OpenSandbox at ${opensandboxUrl || 'not configured'}.`,
23
+ },
24
+ {
25
+ id: 'core.bind-host',
26
+ label: 'Core bind host',
27
+ status: profile.id === 'docker-compose' && env.AI_WORKSTATION_HOST !== '0.0.0.0' ? 'warn' : 'pass',
28
+ summary: `AI_WORKSTATION_HOST=${env.AI_WORKSTATION_HOST || '127.0.0.1'}.`,
29
+ },
30
+ dockerSocketCheck(profile.id),
31
+ workspacePathCheck(config, profile.id),
32
+ allowlistCheck(config, env),
33
+ sandboxImageCheck(config),
34
+ await opensandboxHealthCheck(opensandboxUrl, sandboxProvider.api_key_env, env),
35
+ ];
36
+ return {
37
+ status: summarizeStatus(checks),
38
+ checkedAt,
39
+ checks,
40
+ };
41
+ }
42
+ function dockerSocketCheck(profileId) {
43
+ if (profileId === 'docker-compose') {
44
+ return {
45
+ id: 'docker.socket',
46
+ label: 'Docker socket',
47
+ status: 'pass',
48
+ summary: 'Docker Compose delegates Docker access to OpenSandbox.',
49
+ };
50
+ }
51
+ const socketPath = '/var/run/docker.sock';
52
+ const ok = (0, node_fs_1.existsSync)(socketPath);
53
+ return {
54
+ id: 'docker.socket',
55
+ label: 'Docker socket',
56
+ status: ok ? 'pass' : 'warn',
57
+ summary: ok ? `${socketPath} is available.` : `${socketPath} is not visible to Core.`,
58
+ };
59
+ }
60
+ function workspacePathCheck(config, profileId) {
61
+ const projects = Array.isArray(config.projects) ? config.projects : [];
62
+ const missing = projects
63
+ .map((project) => ({
64
+ name: project.name,
65
+ workDir: String(project.agent?.options?.work_dir || '').trim(),
66
+ sandboxEnabled: Boolean(project.agent?.options?.sandbox?.enabled),
67
+ }))
68
+ .filter((project) => !(profileId === 'docker-compose' && project.sandboxEnabled))
69
+ .filter((project) => project.workDir && !(0, node_fs_1.existsSync)(project.workDir));
70
+ if (missing.length > 0) {
71
+ return {
72
+ id: 'workspace.paths',
73
+ label: 'Workspace paths',
74
+ status: 'warn',
75
+ summary: `${missing.length} configured workspace path(s) are not visible to Core.`,
76
+ };
77
+ }
78
+ if (profileId === 'docker-compose' && projects.some((project) => project.agent?.options?.sandbox?.enabled)) {
79
+ return {
80
+ id: 'workspace.paths',
81
+ label: 'Workspace paths',
82
+ status: 'pass',
83
+ summary: 'Sandbox workspace host paths are delegated to OpenSandbox.',
84
+ };
85
+ }
86
+ return {
87
+ id: 'workspace.paths',
88
+ label: 'Workspace paths',
89
+ status: 'pass',
90
+ summary: projects.length > 0 ? `${projects.length} workspace path(s) checked.` : 'No projects configured.',
91
+ };
92
+ }
93
+ function allowlistCheck(config, env) {
94
+ const allowlist = String(env.OPEN_SANDBOX_HOST_MOUNT_ALLOWLIST || '')
95
+ .split(',')
96
+ .map((item) => item.trim().replace(/\/+$/, ''))
97
+ .filter(Boolean);
98
+ if (allowlist.length === 0) {
99
+ return {
100
+ id: 'opensandbox.allowlist',
101
+ label: 'OpenSandbox mount allowlist',
102
+ status: 'warn',
103
+ summary: 'OPEN_SANDBOX_HOST_MOUNT_ALLOWLIST is not set.',
104
+ };
105
+ }
106
+ const projects = Array.isArray(config.projects) ? config.projects : [];
107
+ const denied = projects.filter((project) => {
108
+ const workDir = String(project.agent?.options?.work_dir || '').trim().replace(/\/+$/, '');
109
+ return workDir && !allowlist.some((root) => workDir === root || workDir.startsWith(`${root}/`));
110
+ });
111
+ return {
112
+ id: 'opensandbox.allowlist',
113
+ label: 'OpenSandbox mount allowlist',
114
+ status: denied.length > 0 ? 'warn' : 'pass',
115
+ summary: denied.length > 0
116
+ ? `${denied.length} project workspace path(s) are outside the allowlist.`
117
+ : `Allowlist covers configured workspace paths: ${allowlist.join(', ')}.`,
118
+ };
119
+ }
120
+ function sandboxImageCheck(config) {
121
+ const images = Array.isArray(config.sandbox_runtime_images) ? config.sandbox_runtime_images : [];
122
+ const missing = images.filter((image) => !String(image.image || '').trim());
123
+ return {
124
+ id: 'sandbox.images',
125
+ label: 'Sandbox images',
126
+ status: missing.length > 0 ? 'fail' : 'pass',
127
+ summary: images.length > 0 ? `${images.length} sandbox image registration(s) configured.` : 'No sandbox image registrations configured.',
128
+ };
129
+ }
130
+ async function opensandboxHealthCheck(serverUrl, apiKeyEnv, env) {
131
+ if (!serverUrl) {
132
+ return {
133
+ id: 'opensandbox.health',
134
+ label: 'OpenSandbox health',
135
+ status: 'fail',
136
+ summary: 'OpenSandbox server URL is empty.',
137
+ };
138
+ }
139
+ try {
140
+ const controller = new AbortController();
141
+ const timer = setTimeout(() => controller.abort(), 3000);
142
+ const apiKey = apiKeyEnv ? env[apiKeyEnv] : undefined;
143
+ const response = await fetch(`${serverUrl}/health`, {
144
+ signal: controller.signal,
145
+ headers: apiKey ? { authorization: `Bearer ${apiKey}` } : undefined,
146
+ });
147
+ clearTimeout(timer);
148
+ return {
149
+ id: 'opensandbox.health',
150
+ label: 'OpenSandbox health',
151
+ status: response.ok ? 'pass' : 'fail',
152
+ summary: `GET ${serverUrl}/health returned HTTP ${response.status}.`,
153
+ };
154
+ }
155
+ catch (error) {
156
+ return {
157
+ id: 'opensandbox.health',
158
+ label: 'OpenSandbox health',
159
+ status: 'fail',
160
+ summary: error instanceof Error ? error.message : String(error),
161
+ };
162
+ }
163
+ }
164
+ function normalizeUrl(value) {
165
+ return String(value || '').trim().replace(/\/+$/, '');
166
+ }
167
+ function summarizeStatus(checks) {
168
+ if (checks.some((check) => check.status === 'fail')) {
169
+ return 'fail';
170
+ }
171
+ if (checks.some((check) => check.status === 'warn')) {
172
+ return 'warn';
173
+ }
174
+ return 'pass';
175
+ }
@@ -2,9 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalCoreController = void 0;
4
4
  const node_events_1 = require("node:events");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
5
8
  const desktop_js_1 = require("../../../../shared/desktop.js");
6
9
  const bootstrap_js_1 = require("../kernel/bootstrap.js");
10
+ const local_core_errors_js_1 = require("../kernel/local-core-errors.js");
7
11
  const runtime_detection_service_js_1 = require("./runtime-detection-service.js");
12
+ const provider_config_migration_js_1 = require("./provider-config-migration.js");
13
+ const deployment_diagnostics_js_1 = require("./deployment-diagnostics.js");
8
14
  class LocalCoreController extends node_events_1.EventEmitter {
9
15
  userDataPath;
10
16
  state;
@@ -13,9 +19,11 @@ class LocalCoreController extends node_events_1.EventEmitter {
13
19
  channelRuntimes;
14
20
  scheduledJobs;
15
21
  automationMonitors;
22
+ store;
16
23
  kernel;
17
24
  runtime;
18
25
  runtimeDetection;
26
+ errorReporter;
19
27
  busUnsubscribers = [];
20
28
  pendingLogs = [];
21
29
  handlingLog = false;
@@ -28,6 +36,7 @@ class LocalCoreController extends node_events_1.EventEmitter {
28
36
  log: (message) => this.handleLog(message),
29
37
  });
30
38
  this.state = this.runtime.state;
39
+ this.store = this.runtime.store;
31
40
  this.kernel = this.runtime.kernel;
32
41
  this.knowledgeProvider = this.runtime.knowledgeProvider;
33
42
  this.workspaceRouter = this.runtime.workspaceRouter;
@@ -43,6 +52,7 @@ class LocalCoreController extends node_events_1.EventEmitter {
43
52
  runMonitorNow: async () => { throw new Error('Automation monitor service is not available.'); },
44
53
  listRuns: () => [],
45
54
  };
55
+ this.errorReporter = new local_core_errors_js_1.LocalCoreErrorReporter((message) => this.handleLog(message));
46
56
  this.runtimeDetection = new runtime_detection_service_js_1.RuntimeDetectionService({
47
57
  userDataPath,
48
58
  readConfig: async () => (await this.readConfigFile()).parsed,
@@ -64,12 +74,20 @@ class LocalCoreController extends node_events_1.EventEmitter {
64
74
  this.emit('automation-monitor-run', run);
65
75
  }), this.kernel.context.bus.on('runtime.state.changed', () => {
66
76
  void this.emitRuntime();
77
+ }), this.kernel.context.bus.on('localcore.error', (event) => {
78
+ const errorInfo = this.errorReporter.report(String(event.scope || 'local-ai-core'), event.errorInfo || event.error || 'Unknown error', event.context || {});
79
+ const runtimeId = String(event.context?.runtimeId || event.context?.agentType || '').trim();
80
+ if (runtimeId) {
81
+ this.runtimeDetection.recordLaunchError(runtimeId, errorInfo);
82
+ }
67
83
  }));
68
84
  }
69
85
  async init() {
70
86
  await this.runtime.start();
71
87
  await this.emitRuntime();
72
- void this.runtimeDetection.refreshOnStartup();
88
+ setTimeout(() => {
89
+ void this.runtimeDetection.refreshOnStartup();
90
+ }, 5000);
73
91
  }
74
92
  async close() {
75
93
  for (const unsubscribe of this.busUnsubscribers) {
@@ -125,7 +143,7 @@ class LocalCoreController extends node_events_1.EventEmitter {
125
143
  return this.state.getLogEntries(level, limit);
126
144
  }
127
145
  async readConfigFile() {
128
- return this.state.readConfigFile();
146
+ return this.readAndMigrateConfigFile();
129
147
  }
130
148
  async saveRawConfigFile(raw) {
131
149
  const next = await this.state.saveRawConfigFile(raw);
@@ -134,17 +152,228 @@ class LocalCoreController extends node_events_1.EventEmitter {
134
152
  return next;
135
153
  }
136
154
  async saveStructuredConfigFile(config) {
137
- const next = await this.state.saveStructuredConfigFile(config);
155
+ const migrated = (0, provider_config_migration_js_1.migrateLegacyProjectProvidersToStore)(config, this.store);
156
+ const next = await this.state.saveStructuredConfigFile(migrated.config);
138
157
  await this.refreshChannelBindings();
139
158
  await this.emitRuntime();
140
159
  return next;
141
160
  }
161
+ async listModelProviders() {
162
+ return { providers: this.store.listModelProviders() };
163
+ }
164
+ async createModelProvider(input) {
165
+ return this.store.upsertModelProvider(input);
166
+ }
167
+ async updateModelProvider(providerId, input) {
168
+ const existing = this.store.getModelProvider(providerId);
169
+ if (!existing) {
170
+ throw new Error(`Provider not found: ${providerId}`);
171
+ }
172
+ return this.store.upsertModelProvider({ ...input, id: providerId });
173
+ }
174
+ async deleteModelProvider(providerId) {
175
+ const config = await this.readAndMigrateConfigFile();
176
+ const referencingProjects = (config.parsed?.projects || [])
177
+ .filter((project) => project.agent?.options?.provider_id === providerId)
178
+ .map((project) => project.name);
179
+ if (referencingProjects.length > 0) {
180
+ throw new Error(`Provider "${providerId}" is still used by projects: ${referencingProjects.join(', ')}`);
181
+ }
182
+ return this.store.deleteModelProvider(providerId);
183
+ }
142
184
  async saveSettings(input) {
143
185
  const settings = await this.state.saveSettings(input);
144
186
  await this.refreshChannelBindings();
145
187
  await this.emitRuntime();
146
188
  return settings;
147
189
  }
190
+ async ensureExternalProject(input) {
191
+ const userId = normalizeExternalId(input.user_id, 'user_id');
192
+ const externalProjectId = normalizeExternalId(input.external_project_id, 'external_project_id');
193
+ const agentType = normalizeExternalSegment(input.agent_type || 'pi', 'pi');
194
+ const existing = this.store.getExternalProject(userId, externalProjectId);
195
+ const provider = this.resolveExternalProvider(input.provider_id || existing?.providerId);
196
+ const workspaceId = externalWorkspaceId(userId, externalProjectId);
197
+ const workspacePath = this.externalProjectBasePath(userId, externalProjectId);
198
+ const displayName = String(input.display_name || externalProjectId).trim() || externalProjectId;
199
+ (0, node_fs_1.mkdirSync)(workspacePath, { recursive: true, mode: 0o700 });
200
+ const now = new Date().toISOString();
201
+ const project = this.store.upsertExternalProject({
202
+ userId,
203
+ externalProjectId,
204
+ workspaceId,
205
+ workspacePath,
206
+ displayName,
207
+ agentType,
208
+ providerId: provider.id,
209
+ metadata: input.metadata || existing?.metadata || {},
210
+ createdAt: existing?.createdAt || now,
211
+ updatedAt: now,
212
+ });
213
+ await this.ensureExternalWorkspaceConfig(project, input.model);
214
+ this.store.upsertWorkspaceRegistryEntry({
215
+ workspaceId: project.workspaceId,
216
+ displayName: project.displayName,
217
+ path: project.workspacePath,
218
+ deviceId: 'external',
219
+ defaultRuntimeId: project.agentType,
220
+ health: { status: 'healthy', summary: 'External workspace is available.', issues: [], checkedAt: now },
221
+ metadata: {
222
+ external: true,
223
+ userId: project.userId,
224
+ externalProjectId: project.externalProjectId,
225
+ },
226
+ });
227
+ return project;
228
+ }
229
+ async createExternalRun(input) {
230
+ if (!String(input.prompt || '').trim()) {
231
+ throw new Error('prompt is required.');
232
+ }
233
+ const project = await this.ensureExternalProject(input);
234
+ const thread = await this.ensureExternalThread(project, input);
235
+ const sent = await this.workspaceRouter.sendThreadMessage(thread.threadId, input.prompt, {
236
+ permissionMode: input.permission_mode,
237
+ runtimeEnv: input.runtime_env,
238
+ });
239
+ const task = this.store.getAgentTaskByRunId(sent.runId);
240
+ return {
241
+ project,
242
+ thread,
243
+ workspace_id: project.workspaceId,
244
+ thread_id: thread.threadId,
245
+ run_id: sent.runId,
246
+ task_id: task?.taskId,
247
+ events_url: `/api/local/v1/external/runs/${encodeURIComponent(sent.runId)}/events`,
248
+ };
249
+ }
250
+ async getExternalRunSnapshot(runId) {
251
+ const task = this.store.getAgentTaskByRunId(runId);
252
+ const run = this.store.getRun(runId);
253
+ const threadId = task?.threadId || run?.thread_id || '';
254
+ const thread = threadId
255
+ ? await this.workspaceRouter.getThread(threadId).catch(() => undefined)
256
+ : undefined;
257
+ return {
258
+ runId,
259
+ task,
260
+ thread,
261
+ };
262
+ }
263
+ async readAndMigrateConfigFile() {
264
+ const current = await this.state.readConfigFile();
265
+ if (!current.parsed) {
266
+ return current;
267
+ }
268
+ const migrated = (0, provider_config_migration_js_1.migrateLegacyProjectProvidersToStore)(current.parsed, this.store);
269
+ if (!migrated.changed) {
270
+ return current;
271
+ }
272
+ const saved = await this.state.saveStructuredConfigFile(migrated.config);
273
+ return {
274
+ ...saved,
275
+ warnings: [
276
+ ...(current.warnings || []),
277
+ ...(saved.warnings || []),
278
+ ...migrated.warnings,
279
+ ],
280
+ };
281
+ }
282
+ resolveExternalProvider(providerId) {
283
+ const requested = String(providerId || '').trim();
284
+ if (requested) {
285
+ const provider = this.store.getModelProvider(requested);
286
+ if (!provider) {
287
+ throw new Error(`Provider not found: ${requested}`);
288
+ }
289
+ return provider;
290
+ }
291
+ const provider = this.store.listModelProviders()[0];
292
+ if (!provider) {
293
+ throw new Error('No model provider is configured. Create a provider before starting an external run.');
294
+ }
295
+ return provider;
296
+ }
297
+ async ensureExternalWorkspaceConfig(project, model) {
298
+ const current = await this.readAndMigrateConfigFile();
299
+ const config = {
300
+ ...(current.parsed || {}),
301
+ projects: Array.isArray(current.parsed?.projects) ? [...current.parsed.projects] : [],
302
+ };
303
+ const existingIndex = config.projects.findIndex((item) => item?.name === project.workspaceId);
304
+ const existing = existingIndex >= 0 ? config.projects[existingIndex] : undefined;
305
+ const defaultRuntimeImageId = Array.isArray(config.sandbox_runtime_images)
306
+ ? config.sandbox_runtime_images.find((image) => image?.agent_type === project.agentType)?.id
307
+ : undefined;
308
+ const options = {
309
+ ...(existing?.agent?.options || {}),
310
+ work_dir: project.workspacePath,
311
+ user_id: project.userId,
312
+ provider_id: project.providerId,
313
+ ...(model ? { model } : {}),
314
+ sandbox: {
315
+ ...(existing?.agent?.options?.sandbox || {}),
316
+ enabled: true,
317
+ ...(defaultRuntimeImageId ? { runtime_image_id: defaultRuntimeImageId } : {}),
318
+ state_scope: 'project',
319
+ sandbox_lifecycle: 'per_thread',
320
+ },
321
+ };
322
+ const nextProject = {
323
+ name: project.workspaceId,
324
+ agent: {
325
+ ...(existing?.agent || {}),
326
+ type: project.agentType,
327
+ options,
328
+ },
329
+ platforms: Array.isArray(existing?.platforms) ? existing.platforms : [],
330
+ admin_from: existing?.admin_from,
331
+ disabled_commands: existing?.disabled_commands,
332
+ };
333
+ if (existingIndex >= 0) {
334
+ config.projects[existingIndex] = nextProject;
335
+ }
336
+ else {
337
+ config.projects.push(nextProject);
338
+ }
339
+ await this.saveStructuredConfigFile(config);
340
+ }
341
+ async ensureExternalThread(project, input) {
342
+ const externalThreadId = normalizeExternalId(input.external_thread_id || `thread-${(0, node_crypto_1.randomUUID)()}`, 'external_thread_id');
343
+ const existing = this.store.getExternalThread(project.userId, project.externalProjectId, externalThreadId);
344
+ if (existing) {
345
+ const thread = await this.workspaceRouter.getThread(existing.threadId).catch(() => undefined);
346
+ if (thread) {
347
+ return existing;
348
+ }
349
+ }
350
+ const workspacePath = this.externalThreadWorkspacePath(project.userId, project.externalProjectId, externalThreadId);
351
+ (0, node_fs_1.mkdirSync)(workspacePath, { recursive: true, mode: 0o700 });
352
+ const title = String(input.title || externalThreadId).trim() || externalThreadId;
353
+ const thread = await this.workspaceRouter.createThread(project.workspaceId, title);
354
+ const now = new Date().toISOString();
355
+ return this.store.upsertExternalThread({
356
+ userId: project.userId,
357
+ externalProjectId: project.externalProjectId,
358
+ externalThreadId,
359
+ workspaceId: project.workspaceId,
360
+ threadId: thread.id,
361
+ workspacePath,
362
+ metadata: input.metadata || {},
363
+ createdAt: now,
364
+ updatedAt: now,
365
+ });
366
+ }
367
+ externalProjectBasePath(userId, externalProjectId) {
368
+ return (0, node_path_1.join)(this.externalWorkspaceRoot(), 'users', normalizeExternalSegment(userId, 'user'), 'projects', normalizeExternalSegment(externalProjectId, 'project'));
369
+ }
370
+ externalThreadWorkspacePath(userId, externalProjectId, externalThreadId) {
371
+ return (0, node_path_1.join)(this.externalProjectBasePath(userId, externalProjectId), 'threads', normalizeExternalSegment(externalThreadId, 'thread'), 'workspace');
372
+ }
373
+ externalWorkspaceRoot() {
374
+ return String(process.env.AGENTDOCK_EXTERNAL_WORKSPACE_ROOT || '').trim()
375
+ || (0, node_path_1.join)(this.userDataPath, 'external-workspaces');
376
+ }
148
377
  async listWorkspaces() {
149
378
  return this.workspaceRouter.listWorkspaces();
150
379
  }
@@ -342,6 +571,77 @@ class LocalCoreController extends node_events_1.EventEmitter {
342
571
  async getPluginDiagnostics() {
343
572
  return this.kernel.diagnostics.snapshot();
344
573
  }
574
+ async listDiagnosticErrors() {
575
+ return this.errorReporter.list();
576
+ }
577
+ async runDiagnosticsDoctor() {
578
+ const checkedAt = new Date().toISOString();
579
+ const configFile = await this.readConfigFile();
580
+ const runtimeChecks = await this.refreshInstalledAgentRuntimes().catch((error) => {
581
+ const errorInfo = (0, local_core_errors_js_1.toLocalCoreErrorInfo)(error, 'internal_error');
582
+ return [{
583
+ agentType: 'runtime',
584
+ runtimeId: 'runtime',
585
+ displayName: 'Runtime Detection',
586
+ status: 'error',
587
+ installed: false,
588
+ detectedAt: checkedAt,
589
+ summary: errorInfo.userMessage,
590
+ issues: [{ code: errorInfo.code, severity: errorInfo.severity, message: errorInfo.message, help: errorInfo.suggestedAction }],
591
+ recommendedActions: errorInfo.suggestedAction ? [{ label: 'Fix runtime detection', description: errorInfo.suggestedAction }] : [],
592
+ source: 'path',
593
+ error: errorInfo.message,
594
+ readiness: 'failed',
595
+ lastLaunchError: errorInfo,
596
+ }];
597
+ });
598
+ const channelStatuses = await this.listChannelGatewayStatuses().catch(() => []);
599
+ const checks = [
600
+ configFile.error
601
+ ? {
602
+ id: 'config',
603
+ label: 'Configuration',
604
+ status: 'fail',
605
+ summary: configFile.error,
606
+ errorInfo: new local_core_errors_js_1.LocalCoreError('config_invalid', configFile.error).info,
607
+ }
608
+ : {
609
+ id: 'config',
610
+ label: 'Configuration',
611
+ status: configFile.exists ? 'pass' : 'warn',
612
+ summary: configFile.exists ? 'Configuration file is readable.' : 'Configuration file has not been created yet.',
613
+ },
614
+ {
615
+ id: 'runtime-detection',
616
+ label: 'Runtime Detection',
617
+ status: runtimeChecks.some((runtime) => runtime.status === 'error') ? 'fail' : 'pass',
618
+ summary: `${runtimeChecks.length} runtime(s) checked.`,
619
+ },
620
+ {
621
+ id: 'channels',
622
+ label: 'Channel Gateways',
623
+ status: channelStatuses.some((status) => status.status === 'error') ? 'warn' : 'pass',
624
+ summary: `${channelStatuses.length} channel gateway status record(s) checked.`,
625
+ errorInfo: channelStatuses.find((status) => status.lastErrorInfo)?.lastErrorInfo,
626
+ },
627
+ {
628
+ id: 'logs',
629
+ label: 'Logs',
630
+ status: 'pass',
631
+ summary: 'Local AI Core log reader is available.',
632
+ },
633
+ ];
634
+ const status = checks.some((check) => check.status === 'fail')
635
+ ? 'fail'
636
+ : checks.some((check) => check.status === 'warn')
637
+ ? 'warn'
638
+ : 'pass';
639
+ return { status, checkedAt, checks };
640
+ }
641
+ async runDeploymentDiagnostics() {
642
+ const configFile = await this.readConfigFile();
643
+ return (0, deployment_diagnostics_js_1.runDeploymentDiagnostics)({ config: configFile.parsed });
644
+ }
345
645
  async probeWorkspaceStreaming(workspaceId) {
346
646
  return this.workspaceRouter.probeWorkspaceStreaming(workspaceId);
347
647
  }
@@ -529,3 +829,30 @@ class LocalCoreController extends node_events_1.EventEmitter {
529
829
  }
530
830
  }
531
831
  exports.LocalCoreController = LocalCoreController;
832
+ function normalizeExternalId(value, label) {
833
+ const normalized = String(value || '').trim();
834
+ if (!normalized) {
835
+ throw new Error(`${label} is required.`);
836
+ }
837
+ return normalized;
838
+ }
839
+ function normalizeExternalSegment(value, fallback) {
840
+ return String(value || '')
841
+ .trim()
842
+ .toLowerCase()
843
+ .replace(/[^a-z0-9._-]+/g, '-')
844
+ .replace(/^-+|-+$/g, '')
845
+ || fallback;
846
+ }
847
+ function externalWorkspaceId(userId, externalProjectId) {
848
+ const base = [
849
+ 'external',
850
+ normalizeExternalSegment(userId, 'user'),
851
+ normalizeExternalSegment(externalProjectId, 'project'),
852
+ ].join('-');
853
+ const digest = (0, node_crypto_1.createHash)('sha256')
854
+ .update(`${userId}\0${externalProjectId}`)
855
+ .digest('hex')
856
+ .slice(0, 8);
857
+ return `${base}-${digest}`;
858
+ }
@@ -38,6 +38,7 @@ const node_fs_1 = require("node:fs");
38
38
  const node_path_1 = require("node:path");
39
39
  const TOML = __importStar(require("@iarna/toml"));
40
40
  const rotating_logger_js_1 = require("../kernel/rotating-logger.js");
41
+ const config_migration_js_1 = require("./config-migration.js");
41
42
  const DEFAULT_CONFIG = `# Managed by Local AI Core
42
43
  # Add [[projects]] entries from the workspace page before starting a conversation.
43
44
  `;
@@ -140,7 +141,14 @@ class FileBackedLocalCoreRuntimeState {
140
141
  const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
141
142
  try {
142
143
  const parsed = TOML.parse(raw);
143
- return { path, exists: true, raw, parsed };
144
+ const migrated = (0, config_migration_js_1.migrateDesktopConnectConfig)(parsed);
145
+ return {
146
+ path,
147
+ exists: true,
148
+ raw,
149
+ parsed: migrated.config,
150
+ warnings: migrated.warnings.length > 0 ? migrated.warnings : undefined,
151
+ };
144
152
  }
145
153
  catch (error) {
146
154
  return {
@@ -158,8 +166,9 @@ class FileBackedLocalCoreRuntimeState {
158
166
  return this.readConfigFile();
159
167
  }
160
168
  async saveStructuredConfigFile(config) {
169
+ const migrated = (0, config_migration_js_1.migrateDesktopConnectConfig)(config);
161
170
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(this.settings.configPath), { recursive: true });
162
- (0, node_fs_1.writeFileSync)(this.settings.configPath, TOML.stringify(config), 'utf8');
171
+ (0, node_fs_1.writeFileSync)(this.settings.configPath, TOML.stringify(migrated.config), 'utf8');
163
172
  return this.readConfigFile();
164
173
  }
165
174
  loadSettings() {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migrateLegacyProjectProvidersToStore = migrateLegacyProjectProvidersToStore;
4
+ const config_migration_js_1 = require("./config-migration.js");
5
+ function migrateLegacyProjectProvidersToStore(input, store) {
6
+ const config = cloneConfig(input);
7
+ const warnings = [];
8
+ let changed = false;
9
+ for (const project of Array.isArray(config.projects) ? config.projects : []) {
10
+ const legacyProviders = Array.isArray(project.agent?.providers) ? project.agent.providers : [];
11
+ if (legacyProviders.length === 0) {
12
+ continue;
13
+ }
14
+ project.agent ||= { type: 'pi', options: {}, providers: [] };
15
+ project.agent.options ||= {};
16
+ const migratedProviderIds = [];
17
+ for (const provider of legacyProviders) {
18
+ const migrated = store.upsertModelProvider((0, config_migration_js_1.normalizeDesktopProviderForStorage)(provider));
19
+ migratedProviderIds.push(migrated.id);
20
+ }
21
+ if (!project.agent.options.provider_id && migratedProviderIds[0]) {
22
+ project.agent.options.provider_id = migratedProviderIds[0];
23
+ }
24
+ delete project.agent.providers;
25
+ warnings.push(`Project "${project.name}" migrated embedded providers to shared providers.`);
26
+ changed = true;
27
+ }
28
+ return { config, changed, warnings };
29
+ }
30
+ function cloneConfig(input) {
31
+ return JSON.parse(JSON.stringify(input || {}));
32
+ }