@sanqianx/claude-ai-workbench 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +129 -0
- package/apps/agent-terminal/app.css +567 -0
- package/apps/agent-terminal/app.js +2392 -0
- package/apps/agent-terminal/index.html +158 -0
- package/apps/ai-profile-settings-host/index.html +25 -0
- package/apps/ai-profile-settings-host/page.css +1 -0
- package/apps/ai-profile-settings-host/page.js +113 -0
- package/apps/minimal-test-host/README.md +15 -0
- package/apps/minimal-test-host/host.css +2 -0
- package/apps/minimal-test-host/host.js +35 -0
- package/apps/minimal-test-host/index.html +58 -0
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -0
- package/docs/sdk/FILE_LAYOUT.md +31 -0
- package/docs/sdk/INTEGRATION.md +126 -0
- package/docs/sdk/README.md +21 -0
- package/docs/sdk/openapi.yaml +760 -0
- package/package.json +51 -0
- package/packages/client/README.md +11 -0
- package/packages/client/index.d.ts +81 -0
- package/packages/client/index.js +198 -0
- package/packages/client/package.json +29 -0
- package/packages/contracts/README.md +9 -0
- package/packages/contracts/index.d.ts +252 -0
- package/packages/contracts/index.js +295 -0
- package/packages/contracts/package.json +24 -0
- package/packages/core/README.md +8 -0
- package/packages/core/index.d.ts +76 -0
- package/packages/core/index.js +214 -0
- package/packages/core/package.json +29 -0
- package/packages/server/README.md +17 -0
- package/packages/server/bin/agent-terminal-server.js +36 -0
- package/packages/server/bin/claude-workbench-server.js +46 -0
- package/packages/server/bin/claude-workbench.js +225 -0
- package/packages/server/index.d.ts +145 -0
- package/packages/server/index.js +183 -0
- package/packages/server/lib/agent-discovery.js +112 -0
- package/packages/server/lib/agent-registry.js +98 -0
- package/packages/server/lib/attachment-store.js +78 -0
- package/packages/server/lib/bridges/app-server-bridge.js +372 -0
- package/packages/server/lib/bridges/opencode-server-bridge.js +490 -0
- package/packages/server/lib/capture-settings-store.js +34 -0
- package/packages/server/lib/context-resolver.js +40 -0
- package/packages/server/lib/drivers/cli-json-runner.js +960 -0
- package/packages/server/lib/drivers/codex-driver.js +1413 -0
- package/packages/server/lib/drivers/opencode-driver.js +1527 -0
- package/packages/server/lib/drivers/zcode-driver.js +1167 -0
- package/packages/server/lib/event-stream.js +35 -0
- package/packages/server/lib/folder-picker.js +623 -0
- package/packages/server/lib/http-handler.js +254 -0
- package/packages/server/lib/json-file.js +19 -0
- package/packages/server/lib/model-detection.js +295 -0
- package/packages/server/lib/profile-store.js +149 -0
- package/packages/server/lib/project-store.js +102 -0
- package/packages/server/lib/session-archive-store.js +53 -0
- package/packages/server/lib/system-doctor.js +119 -0
- package/packages/server/lib/ui-preferences-store.js +48 -0
- package/packages/server/lib/workbench-service.js +620 -0
- package/packages/server/package.json +34 -0
- package/packages/ui/README.md +30 -0
- package/packages/ui/ai-profile-settings.d.ts +45 -0
- package/packages/ui/ai-profile-settings.js +283 -0
- package/packages/ui/claude-terminal.d.ts +31 -0
- package/packages/ui/claude-terminal.js +343 -0
- package/packages/ui/index.d.ts +18 -0
- package/packages/ui/index.js +51 -0
- package/packages/ui/package.json +42 -0
- package/packages/ui/theme.css +13 -0
- package/src/assets/claude-prompts.json +21 -0
- package/src/backend/http/ai-profile-routes.js +61 -0
- package/src/backend/http/claude-routes.js +170 -0
- package/src/backend/http/node-helpers.js +45 -0
- package/src/backend/index.js +21 -0
- package/src/backend/lib/ai-adapter.js +144 -0
- package/src/backend/lib/ai-profile-store.js +259 -0
- package/src/backend/lib/ai-vendor-presets.js +83 -0
- package/src/backend/lib/ai-workspace.js +140 -0
- package/src/backend/lib/automation-config.js +253 -0
- package/src/backend/lib/claude-cli-runner.js +1749 -0
- package/src/backend/lib/data-dir.js +169 -0
- package/src/backend/lib/llm-client.js +176 -0
- package/src/backend/lib/model-context-windows.js +82 -0
- package/src/backend/lib/prompt-registry.js +166 -0
- package/src/backend/lib/runtime-config.js +121 -0
- package/src/index.js +7 -0
- package/src/sdk-wave-a.ts +1 -0
- package/src/sdk-wave-b.ts +1 -0
- package/src/sdk-wave-c.ts +1 -0
- package/src/sdk-wave-d.ts +1 -0
- package/src/sdk-wave-e.ts +1 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { normalizeProfile, cleanId } = require('../../contracts');
|
|
5
|
+
const { readJson, writeJson } = require('./json-file');
|
|
6
|
+
|
|
7
|
+
class ProfileStore {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
if (!options.dataDir) throw new Error('ProfileStore dataDir is required');
|
|
10
|
+
this.profileFile = options.profileFile || path.join(options.dataDir, 'profiles.v2.json');
|
|
11
|
+
this.credentialFile = options.credentialFile || path.join(options.dataDir, 'credentials.v1.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_profiles() { return readJson(this.profileFile, { schema: 'claude-workbench-profiles/v2', profiles: [] }); }
|
|
15
|
+
_credentials() { return readJson(this.credentialFile, { schema: 'claude-workbench-credentials/v1', credentials: {} }); }
|
|
16
|
+
|
|
17
|
+
_configured(profile) {
|
|
18
|
+
return Boolean(this._credentials().credentials[profile.credentialRef]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
publicProfile(profile) {
|
|
22
|
+
profile = normalizeProfile(profile);
|
|
23
|
+
const secret = this._credentials().credentials[profile.credentialRef] || '';
|
|
24
|
+
const clean = {
|
|
25
|
+
...profile,
|
|
26
|
+
credentialConfigured: Boolean(secret),
|
|
27
|
+
credentialMasked: maskCredential(secret),
|
|
28
|
+
};
|
|
29
|
+
delete clean.apiKey;
|
|
30
|
+
delete clean.authToken;
|
|
31
|
+
delete clean.anthropicAuthToken;
|
|
32
|
+
delete clean.secret;
|
|
33
|
+
return clean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
list() { return this._profiles().profiles.map(profile => this.publicProfile(profile)); }
|
|
37
|
+
|
|
38
|
+
get(id) {
|
|
39
|
+
const profileId = cleanId(id, 'profileId');
|
|
40
|
+
const profile = this._profiles().profiles.find(item => item.id === profileId);
|
|
41
|
+
return profile ? this.publicProfile(profile) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
save(input, idOverride) {
|
|
45
|
+
const profile = normalizeProfile(input, idOverride);
|
|
46
|
+
validateProfile(profile);
|
|
47
|
+
const cfg = this._profiles();
|
|
48
|
+
const index = cfg.profiles.findIndex(item => item.id === profile.id);
|
|
49
|
+
if (index >= 0) cfg.profiles[index] = profile;
|
|
50
|
+
else cfg.profiles.push(profile);
|
|
51
|
+
writeJson(this.profileFile, cfg);
|
|
52
|
+
return this.publicProfile(profile);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
delete(id) {
|
|
56
|
+
const profileId = cleanId(id, 'profileId');
|
|
57
|
+
const cfg = this._profiles();
|
|
58
|
+
const profile = cfg.profiles.find(item => item.id === profileId);
|
|
59
|
+
if (!profile) return false;
|
|
60
|
+
cfg.profiles = cfg.profiles.filter(item => item.id !== profileId);
|
|
61
|
+
writeJson(this.profileFile, cfg);
|
|
62
|
+
const secrets = this._credentials();
|
|
63
|
+
delete secrets.credentials[profile.credentialRef];
|
|
64
|
+
writeJson(this.credentialFile, secrets);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setCredential(id, credential) {
|
|
69
|
+
const profileId = cleanId(id, 'profileId');
|
|
70
|
+
const cfg = this._profiles();
|
|
71
|
+
const profile = cfg.profiles.find(item => item.id === profileId);
|
|
72
|
+
if (!profile) throw Object.assign(new Error('profile not found'), { status: 404 });
|
|
73
|
+
const secret = typeof credential === 'string' ? credential : credential && credential.secret;
|
|
74
|
+
if (!secret || typeof secret !== 'string') {
|
|
75
|
+
throw Object.assign(new Error('credential secret is required'), { status: 400 });
|
|
76
|
+
}
|
|
77
|
+
const secrets = this._credentials();
|
|
78
|
+
secrets.credentials[profile.credentialRef] = String(secret);
|
|
79
|
+
writeJson(this.credentialFile, secrets);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
runtimeProfile(id) {
|
|
83
|
+
const profileId = cleanId(id, 'profileId');
|
|
84
|
+
const profile = this._profiles().profiles.find(item => item.id === profileId);
|
|
85
|
+
if (!profile) throw Object.assign(new Error('profile not found'), { status: 404 });
|
|
86
|
+
if (!profile.enabled) throw Object.assign(new Error('profile is disabled'), { status: 400 });
|
|
87
|
+
const secret = this._credentials().credentials[profile.credentialRef];
|
|
88
|
+
if (!secret) throw Object.assign(new Error('profile credential is not configured'), { status: 400 });
|
|
89
|
+
return runtimeShape(profile, secret);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
runtimeDraft(input, credential, options = {}) {
|
|
93
|
+
const profile = normalizeProfile(input);
|
|
94
|
+
validateProfile(profile, options);
|
|
95
|
+
if (!profile.enabled) throw Object.assign(new Error('profile is disabled'), { status: 400 });
|
|
96
|
+
let secret = typeof credential === 'string' ? credential : credential && credential.secret;
|
|
97
|
+
if (!secret) {
|
|
98
|
+
const saved = this._profiles().profiles.find(item => item.id === profile.id);
|
|
99
|
+
if (saved) secret = this._credentials().credentials[saved.credentialRef];
|
|
100
|
+
}
|
|
101
|
+
if (!secret) throw Object.assign(new Error('profile credential is not configured'), { status: 400 });
|
|
102
|
+
return runtimeShape(profile, String(secret));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runtimeShape(profile, secret) {
|
|
107
|
+
profile = normalizeProfile(profile);
|
|
108
|
+
return {
|
|
109
|
+
...profile,
|
|
110
|
+
apiKey: secret,
|
|
111
|
+
mainModel: profile.models.default,
|
|
112
|
+
thinkingModel: profile.models.reasoning || profile.models.default,
|
|
113
|
+
haikuModel: profile.models.fast || profile.models.default,
|
|
114
|
+
sonnetModel: profile.models.coding || profile.models.default,
|
|
115
|
+
opusModel: profile.models.largeContext || profile.models.default,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function maskCredential(secret) {
|
|
120
|
+
const value = String(secret || '');
|
|
121
|
+
if (!value) return '';
|
|
122
|
+
if (value.length <= 12) return `${value.slice(0, 2)}****${value.slice(-2)}`;
|
|
123
|
+
return `${value.slice(0, 6)}****${value.slice(-6)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateProfile(profile, options = {}) {
|
|
127
|
+
if (profile.runtime !== 'claude-code') throw Object.assign(new Error('runtime must be claude-code'), { status: 400 });
|
|
128
|
+
if (!profile.provider) throw Object.assign(new Error('provider is required'), { status: 400 });
|
|
129
|
+
// One protocol endpoint is enough: single-protocol providers are valid;
|
|
130
|
+
// the agents that speak the missing protocol simply can't use it.
|
|
131
|
+
if (!profile.baseUrl && !profile.openaiBaseUrl) {
|
|
132
|
+
throw Object.assign(new Error('baseUrl is required'), { status: 400 });
|
|
133
|
+
}
|
|
134
|
+
for (const field of ['baseUrl', 'openaiBaseUrl']) {
|
|
135
|
+
if (!profile[field]) continue;
|
|
136
|
+
let url;
|
|
137
|
+
try { url = new URL(profile[field]); } catch {}
|
|
138
|
+
if (!url || !['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
139
|
+
throw Object.assign(new Error(`${field} must be an HTTP(S) endpoint without credentials, query, or fragment`), { status: 400 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Detection runs before any model is configured; every other path needs one.
|
|
143
|
+
if (!profile.models.default && options.requireModel !== false) {
|
|
144
|
+
throw Object.assign(new Error('models.default is required'), { status: 400 });
|
|
145
|
+
}
|
|
146
|
+
return profile;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { ProfileStore, validateProfile, maskCredential };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { normalizeProject } = require('../../contracts');
|
|
7
|
+
const { readJson, writeJson } = require('./json-file');
|
|
8
|
+
|
|
9
|
+
const SCHEMA = 'agent-terminal-projects/v1';
|
|
10
|
+
|
|
11
|
+
// Registry of user-created workspaces. Each project binds a display name to
|
|
12
|
+
// an absolute local directory; its projectId doubles as the ContextResolver
|
|
13
|
+
// workspaceRef so sessions inherit project ownership for free.
|
|
14
|
+
class ProjectStore {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
if (!options.dataDir) throw new Error('ProjectStore dataDir is required');
|
|
17
|
+
this.projectFile = options.projectFile || path.join(options.dataDir, 'projects.v1.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_read() { return readJson(this.projectFile, { schema: SCHEMA, projects: [] }); }
|
|
21
|
+
_write(cfg) { writeJson(this.projectFile, cfg); }
|
|
22
|
+
|
|
23
|
+
list() {
|
|
24
|
+
return this._read().projects
|
|
25
|
+
.slice()
|
|
26
|
+
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(id) {
|
|
30
|
+
return this._read().projects.find(item => item.id === id) || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
create(input = {}) {
|
|
34
|
+
const cfg = this._read();
|
|
35
|
+
const rootPath = resolveExistingDirectory(input.rootPath);
|
|
36
|
+
const project = normalizeProject({ ...input, rootPath }, generateProjectId(cfg));
|
|
37
|
+
cfg.projects.push(project);
|
|
38
|
+
this._write(cfg);
|
|
39
|
+
return project;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
rename(id, name) {
|
|
43
|
+
return this.update(id, { name });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
update(id, input = {}) {
|
|
47
|
+
const cfg = this._read();
|
|
48
|
+
const index = cfg.projects.findIndex(item => item.id === id);
|
|
49
|
+
if (index < 0) throw Object.assign(new Error('project not found'), { status: 404 });
|
|
50
|
+
const current = cfg.projects[index];
|
|
51
|
+
const next = normalizeProject({
|
|
52
|
+
...current,
|
|
53
|
+
...Object.fromEntries(['name', 'defaultAiProfileId', 'defaultModel', 'defaultAgentId']
|
|
54
|
+
.filter(key => Object.prototype.hasOwnProperty.call(input, key)).map(key => [key, input[key]])),
|
|
55
|
+
rootPath: current.rootPath, createdAt: current.createdAt,
|
|
56
|
+
}, current.id);
|
|
57
|
+
next.updatedAt = new Date().toISOString();
|
|
58
|
+
cfg.projects[index] = next;
|
|
59
|
+
this._write(cfg);
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
remove(id) {
|
|
64
|
+
const cfg = this._read();
|
|
65
|
+
if (!cfg.projects.some(item => item.id === id)) return false;
|
|
66
|
+
cfg.projects = cfg.projects.filter(item => item.id !== id);
|
|
67
|
+
this._write(cfg);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
toWorkspaceMap() {
|
|
72
|
+
const map = {};
|
|
73
|
+
for (const project of this.list()) map[project.id] = project.rootPath;
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function generateProjectId(cfg) {
|
|
79
|
+
const taken = new Set(cfg.projects.map(item => item.id));
|
|
80
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
81
|
+
const id = `prj-${crypto.randomBytes(5).toString('hex')}`;
|
|
82
|
+
if (!taken.has(id)) return id;
|
|
83
|
+
}
|
|
84
|
+
throw new Error('unable to allocate a project id');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveExistingDirectory(rootPath) {
|
|
88
|
+
const value = String(rootPath || '').trim();
|
|
89
|
+
if (!value) throw Object.assign(new Error('project rootPath is required'), { status: 400 });
|
|
90
|
+
let real;
|
|
91
|
+
try {
|
|
92
|
+
real = fs.realpathSync(path.resolve(value));
|
|
93
|
+
} catch {
|
|
94
|
+
throw Object.assign(new Error(`project directory does not exist: ${value}`), { status: 400 });
|
|
95
|
+
}
|
|
96
|
+
if (!fs.statSync(real).isDirectory()) {
|
|
97
|
+
throw Object.assign(new Error(`project rootPath is not a directory: ${value}`), { status: 400 });
|
|
98
|
+
}
|
|
99
|
+
return real;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { ProjectStore };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readJson, writeJson } = require('./json-file');
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'agent-terminal-session-archive/v1';
|
|
7
|
+
|
|
8
|
+
// Sidecar marking sessions the user archived. Session records themselves stay
|
|
9
|
+
// owned by the runner, so archiving never rewrites runner history — it only
|
|
10
|
+
// flips visibility in listings and can be undone at any time.
|
|
11
|
+
class SessionArchiveStore {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (!options.dataDir) throw new Error('SessionArchiveStore dataDir is required');
|
|
14
|
+
this.archiveFile = options.archiveFile || path.join(options.dataDir, 'archived-sessions.v1.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_read() { return readJson(this.archiveFile, { schema: SCHEMA, sessions: {} }); }
|
|
18
|
+
_write(cfg) { writeJson(this.archiveFile, cfg); }
|
|
19
|
+
|
|
20
|
+
isArchived(sessionId) { return Boolean(this._read().sessions[sessionId]); }
|
|
21
|
+
// Marks older than maxAgeMs. Entries without a parseable archivedAt never
|
|
22
|
+
// expire — deleting what cannot be dated is not a safe default.
|
|
23
|
+
expired(maxAgeMs, now = Date.now()) {
|
|
24
|
+
const cfg = this._read();
|
|
25
|
+
const ids = [];
|
|
26
|
+
for (const [sessionId, meta] of Object.entries(cfg.sessions)) {
|
|
27
|
+
const archivedAt = Date.parse(meta && meta.archivedAt);
|
|
28
|
+
if (Number.isFinite(archivedAt) && now - archivedAt > maxAgeMs) ids.push(sessionId);
|
|
29
|
+
}
|
|
30
|
+
return ids;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
archive(sessionId) {
|
|
34
|
+
const cfg = this._read();
|
|
35
|
+
if (!cfg.sessions[sessionId]) {
|
|
36
|
+
cfg.sessions[sessionId] = { archivedAt: new Date().toISOString() };
|
|
37
|
+
this._write(cfg);
|
|
38
|
+
}
|
|
39
|
+
return cfg.sessions[sessionId];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
restore(sessionId) {
|
|
43
|
+
const cfg = this._read();
|
|
44
|
+
if (!cfg.sessions[sessionId]) return false;
|
|
45
|
+
delete cfg.sessions[sessionId];
|
|
46
|
+
this._write(cfg);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
forget(sessionId) { this.restore(sessionId); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { SessionArchiveStore };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Environment diagnostics behind GET /system/doctor. Everything a fresh
|
|
9
|
+
// machine needs to explain "discovery showed the agent but turns failed":
|
|
10
|
+
// resolved executables, live version probes, login-state files — no secrets.
|
|
11
|
+
function createSystemDoctor(deps = {}) {
|
|
12
|
+
const resolveClaudeExecutable = deps.resolveClaudeExecutable || (() => ({ cmd: null }));
|
|
13
|
+
const discoverZCodeCli = deps.discoverZCodeCli || (() => null);
|
|
14
|
+
const probeZCodeVersion = deps.probeZCodeVersion || (() => null);
|
|
15
|
+
const doSpawnSync = deps.spawnSyncImpl || spawnSync;
|
|
16
|
+
const env = deps.env || process.env;
|
|
17
|
+
const platform = deps.platform || process.platform;
|
|
18
|
+
|
|
19
|
+
function probe(command, args) {
|
|
20
|
+
try {
|
|
21
|
+
const result = doSpawnSync(command, args, {
|
|
22
|
+
timeout: 10000,
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
shell: platform === 'win32',
|
|
26
|
+
env,
|
|
27
|
+
});
|
|
28
|
+
if (result.error) {
|
|
29
|
+
return { ok: false, error: String(result.error.message || result.error).slice(0, 200) };
|
|
30
|
+
}
|
|
31
|
+
const text = String(result.stdout || result.stderr || '').trim();
|
|
32
|
+
if (result.status !== 0) {
|
|
33
|
+
return { ok: false, exitCode: result.status, error: text.slice(0, 200) || `exit ${result.status}` };
|
|
34
|
+
}
|
|
35
|
+
const match = text.match(/(\d+\.\d+[\w.-]*)/);
|
|
36
|
+
return { ok: true, version: match ? match[1] : text.split(/\r?\n/)[0].slice(0, 64) || null };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { ok: false, error: String(error && error.message || error).slice(0, 200) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fileState(file) {
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.statSync(file);
|
|
45
|
+
return { exists: true, sizeBytes: stat.size };
|
|
46
|
+
} catch {
|
|
47
|
+
return { exists: false };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function zcodeLoginState() {
|
|
52
|
+
const home = os.homedir();
|
|
53
|
+
const config = path.join(home, '.zcode', 'v2', 'config.json');
|
|
54
|
+
const state = { config: fileState(config), providers: [] };
|
|
55
|
+
if (!state.config.exists) return state;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(config, 'utf8'));
|
|
58
|
+
const scan = (node) => {
|
|
59
|
+
if (!node || typeof node !== 'object') return;
|
|
60
|
+
for (const [key, value] of Object.entries(node)) {
|
|
61
|
+
if (/provider|coding|glm|anthropic|openai/i.test(key) && value && typeof value === 'object') {
|
|
62
|
+
state.providers.push(key);
|
|
63
|
+
} else if (typeof value === 'object') {
|
|
64
|
+
scan(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
scan(parsed);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
state.parseError = String(error && error.message || error).slice(0, 120);
|
|
71
|
+
}
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function claudeLoginState() {
|
|
76
|
+
const home = os.homedir();
|
|
77
|
+
return {
|
|
78
|
+
claudeJson: fileState(path.join(home, '.claude.json')),
|
|
79
|
+
claudeDir: fileState(path.join(home, '.claude')),
|
|
80
|
+
execPathOverride: env.CLAUDE_CODE_EXECPATH || null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return function doctor() {
|
|
85
|
+
const claude = resolveClaudeExecutable() || {};
|
|
86
|
+
const claudeProbe = claude.cmd
|
|
87
|
+
? probe(claude.cmd, ['--version'])
|
|
88
|
+
: { ok: false, error: claude.reason || 'no executable resolved' };
|
|
89
|
+
const zcode = discoverZCodeCli();
|
|
90
|
+
const report = {
|
|
91
|
+
schema: 'agent-terminal-doctor/v1',
|
|
92
|
+
at: new Date().toISOString(),
|
|
93
|
+
environment: {
|
|
94
|
+
node: process.version,
|
|
95
|
+
platform,
|
|
96
|
+
arch: process.arch,
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
},
|
|
99
|
+
agents: {
|
|
100
|
+
'claude-code': {
|
|
101
|
+
resolvedExecutable: claude.cmd || null,
|
|
102
|
+
resolutionReason: claude.cmd ? null : (claude.reason || 'not found'),
|
|
103
|
+
versionProbe: claudeProbe,
|
|
104
|
+
loginState: claudeLoginState(),
|
|
105
|
+
},
|
|
106
|
+
codex: { versionProbe: probe('codex', ['--version']) },
|
|
107
|
+
opencode: { versionProbe: probe('opencode', ['--version']) },
|
|
108
|
+
zcode: {
|
|
109
|
+
cli: zcode ? { path: zcode.path, source: zcode.source || null } : null,
|
|
110
|
+
versionProbe: zcode ? probe(process.execPath, [zcode.path, '--version']) : { ok: false, error: 'CLI not discovered' },
|
|
111
|
+
loginState: zcodeLoginState(),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
return report;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { createSystemDoctor };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readJson, writeJson } = require('./json-file');
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'agent-terminal-ui-preferences/v1';
|
|
7
|
+
|
|
8
|
+
// Last-used composer selector values (protection mode, model, effort, agent).
|
|
9
|
+
// The UI loads them on boot and stores every user-driven change back, so the
|
|
10
|
+
// selections survive conversation/project switches and full restarts. Only
|
|
11
|
+
// whitelisted string keys are kept, so a stale or hostile payload cannot
|
|
12
|
+
// smuggle arbitrary state into the renderer.
|
|
13
|
+
const ALLOWED_KEYS = ['permissionMode', 'effort', 'currentAgent', 'currentProfileId', 'currentModel'];
|
|
14
|
+
|
|
15
|
+
class UiPreferencesStore {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
if (!options.dataDir) throw new Error('UiPreferencesStore dataDir is required');
|
|
18
|
+
this.preferencesFile = options.preferencesFile || path.join(options.dataDir, 'ui-preferences.v1.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
read() {
|
|
22
|
+
const cfg = readJson(this.preferencesFile, { schema: SCHEMA, values: {} });
|
|
23
|
+
return { schema: SCHEMA, values: this._clean(cfg.values || {}) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
save(input = {}) {
|
|
27
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
28
|
+
throw Object.assign(new Error('ui preferences must be an object'), { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
const values = { ...this.read().values, ...this._clean(input) };
|
|
31
|
+
const cfg = { schema: SCHEMA, values, updatedAt: new Date().toISOString() };
|
|
32
|
+
writeJson(this.preferencesFile, cfg);
|
|
33
|
+
return { schema: SCHEMA, values };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_clean(input) {
|
|
37
|
+
const values = {};
|
|
38
|
+
for (const key of ALLOWED_KEYS) {
|
|
39
|
+
if (input[key] === undefined) continue;
|
|
40
|
+
if (input[key] !== null && typeof input[key] !== 'string') continue;
|
|
41
|
+
if (typeof input[key] === 'string' && input[key].length > 200) continue;
|
|
42
|
+
values[key] = input[key];
|
|
43
|
+
}
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { UiPreferencesStore, SCHEMA };
|