@kendoo.agentdesk/agentdesk 0.15.5 → 0.15.6
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/CHANGELOG.md +5 -0
- package/cli/config.mjs +116 -48
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@ All user-facing changes to AgentDesk. Each entry is tagged:
|
|
|
8
8
|
|
|
9
9
|
Internal refactors, infrastructure changes, and architectural notes are not listed here.
|
|
10
10
|
|
|
11
|
+
## [0.15.6] — 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `[CLI]` `.agentdesk.json` is now kept in sync with the server on every CLI run — after fetching the latest settings, the local file is rewritten as a cached view of the authoritative state. Credentials never land on disk. If you edited a field locally and the server had a different value for it, a one-line warning tells you which fields the server overrode so silent drift doesn't hide changes.
|
|
15
|
+
|
|
11
16
|
## [0.15.5] — 2026-04-18
|
|
12
17
|
|
|
13
18
|
### Added
|
package/cli/config.mjs
CHANGED
|
@@ -1,58 +1,44 @@
|
|
|
1
1
|
// .agentdesk.json config loader
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
// Flow:
|
|
3
|
+
// 1. Pull from server (authoritative).
|
|
4
|
+
// 2. Merge DEFAULTS ← local ← server.
|
|
5
|
+
// 3. If local and server both had values for the same non-credential field
|
|
6
|
+
// but disagreed, print a one-line warning so the user notices server
|
|
7
|
+
// overrode their edit.
|
|
8
|
+
// 4. Write the merged non-credential config back to .agentdesk.json so the
|
|
9
|
+
// on-disk file stays a cached view of the server. Credentials never land
|
|
10
|
+
// on disk — they stay in the server DB, encrypted.
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
13
|
import { join } from "path";
|
|
6
14
|
|
|
7
15
|
const DEFAULTS = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
linear: {
|
|
13
|
-
teamKey: null, // e.g., "KEN"
|
|
14
|
-
workspace: null, // e.g., "kendoo" — the slug in linear.app/<workspace>/issue/...
|
|
15
|
-
},
|
|
16
|
-
jira: {
|
|
17
|
-
baseUrl: null, // e.g., "https://mycompany.atlassian.net"
|
|
18
|
-
project: null, // e.g., "PROJ"
|
|
19
|
-
},
|
|
20
|
-
github: {
|
|
21
|
-
repo: null, // e.g., "owner/repo" — auto-detected from git if null
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
// Team composition — array of agent names or custom agent objects
|
|
25
|
-
// Default: all 8 agents. To customize:
|
|
26
|
-
// "team": ["Jane", "Dennis", "Sam", "Bart"] — 4-agent team (no Luna, Mark, Vera, Nora)
|
|
27
|
-
// "team": ["Jane", "Dennis", "Sam", {"name": "Alex", "role": "DevOps Engineer", "description": "..."}]
|
|
28
|
-
// Jane, Dennis, and Sam are always included even if omitted.
|
|
16
|
+
tracker: null,
|
|
17
|
+
linear: { teamKey: null, workspace: null },
|
|
18
|
+
jira: { baseUrl: null, project: null },
|
|
19
|
+
github: { repo: null },
|
|
29
20
|
team: null,
|
|
30
|
-
|
|
31
|
-
// Commands — auto-detected if null
|
|
32
|
-
commands: {
|
|
33
|
-
test: null,
|
|
34
|
-
build: null,
|
|
35
|
-
lint: null,
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
// Existing agents/bots in the project — declare them so the team knows about them
|
|
39
|
-
// Each entry: { name, role, when, how }
|
|
21
|
+
commands: { test: null, build: null, lint: null },
|
|
40
22
|
projectAgents: [],
|
|
41
|
-
|
|
42
|
-
// Capture screenshots for UI tasks (default: on)
|
|
43
23
|
screenshots: true,
|
|
44
|
-
|
|
45
24
|
// Model per phase — override the phase default.
|
|
46
25
|
// Keys: INTAKE, PLAN, EXECUTION, REVIEW, SUMMARY. Values: "default" | "opus" | "sonnet" | "haiku".
|
|
47
26
|
// Missing entry or "default" falls back to the phase default
|
|
48
27
|
// (sonnet for INTAKE/PLAN/EXECUTION, haiku for REVIEW/SUMMARY).
|
|
49
|
-
// Example: { "PLAN": "opus", "EXECUTION": "opus", "REVIEW": "sonnet" }
|
|
50
28
|
phaseModels: {},
|
|
51
|
-
|
|
52
|
-
// Extra prompt instructions appended to the team prompt
|
|
53
29
|
instructions: null,
|
|
54
30
|
};
|
|
55
31
|
|
|
32
|
+
// Fields that must never be written to .agentdesk.json. Credentials live only
|
|
33
|
+
// in the server DB, encrypted, and are fetched fresh per-session via the
|
|
34
|
+
// /credentials endpoint.
|
|
35
|
+
const CREDENTIAL_PATHS = [
|
|
36
|
+
["linear", "apiKey"],
|
|
37
|
+
["jira", "email"],
|
|
38
|
+
["jira", "apiToken"],
|
|
39
|
+
["github", "token"],
|
|
40
|
+
];
|
|
41
|
+
|
|
56
42
|
async function fetchServerConfig(projectName, apiKey, serverUrl) {
|
|
57
43
|
if (!projectName || !apiKey || !serverUrl) return null;
|
|
58
44
|
try {
|
|
@@ -69,37 +55,119 @@ async function fetchServerConfig(projectName, apiKey, serverUrl) {
|
|
|
69
55
|
}
|
|
70
56
|
|
|
71
57
|
export async function loadConfig(dir, opts = {}) {
|
|
72
|
-
const { apiKey, serverUrl, projectName } = opts;
|
|
58
|
+
const { apiKey, serverUrl, projectName, silent = false } = opts;
|
|
59
|
+
const configPath = join(dir, ".agentdesk.json");
|
|
73
60
|
|
|
74
61
|
// Load local .agentdesk.json
|
|
75
|
-
const configPath = join(dir, ".agentdesk.json");
|
|
76
62
|
let localConfig = null;
|
|
77
63
|
if (existsSync(configPath)) {
|
|
78
64
|
try {
|
|
79
65
|
localConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
80
66
|
} catch (err) {
|
|
81
|
-
console.error(`Warning: Failed to parse .agentdesk.json: ${err.message}`);
|
|
67
|
+
if (!silent) console.error(`Warning: Failed to parse .agentdesk.json: ${err.message}`);
|
|
82
68
|
}
|
|
83
69
|
}
|
|
84
70
|
|
|
85
71
|
// Fetch server settings
|
|
86
72
|
const serverConfig = await fetchServerConfig(projectName, apiKey, serverUrl);
|
|
87
73
|
|
|
74
|
+
// Warn on real conflicts before merging: a field the user set locally that
|
|
75
|
+
// the server overrides with a different value. "Server wins" is the rule,
|
|
76
|
+
// but silent override is the pattern we're trying to stop.
|
|
77
|
+
if (!silent && localConfig && serverConfig) {
|
|
78
|
+
const conflicts = findConflicts(localConfig, serverConfig);
|
|
79
|
+
if (conflicts.length > 0) {
|
|
80
|
+
const sample = conflicts.slice(0, 3).map(c => ` ${c.path}: local=${JSON.stringify(c.local)}, server=${JSON.stringify(c.server)}`).join("\n");
|
|
81
|
+
const extra = conflicts.length > 3 ? `\n …and ${conflicts.length - 3} more` : "";
|
|
82
|
+
console.error(`Note: server config overrode local edits in .agentdesk.json:\n${sample}${extra}\nEdit via the UI or re-run \`agentdesk init\` to push local changes.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
88
86
|
// Merge: DEFAULTS ← local ← server (UI is the single source of truth)
|
|
89
87
|
let config = { ...DEFAULTS };
|
|
90
88
|
if (localConfig) config = deepMerge(config, localConfig);
|
|
91
89
|
if (serverConfig) config = deepMerge(config, serverConfig);
|
|
92
90
|
|
|
93
|
-
//
|
|
91
|
+
// First-time sync: if local exists but server is empty, push local up.
|
|
94
92
|
if (localConfig && !serverConfig && apiKey && serverUrl && projectName) {
|
|
95
|
-
|
|
93
|
+
pushConfig(apiKey, serverUrl, projectName, localConfig).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Cache the merged, credential-free config back to disk so the local file
|
|
97
|
+
// stays a readable view of the authoritative state. Only when we actually
|
|
98
|
+
// fetched from the server — offline runs must not silently mutate the
|
|
99
|
+
// user's local file.
|
|
100
|
+
if (serverConfig) {
|
|
101
|
+
writeLocalConfig(configPath, config);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Push a config object to the server. Caller is responsible for deciding when.
|
|
108
|
+
export async function pushConfig(apiKey, serverUrl, projectName, payload) {
|
|
109
|
+
if (!apiKey || !serverUrl || !projectName) return { ok: false, error: "missing_auth" };
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`${serverUrl}/api/projects/${projectName}/settings`, {
|
|
96
112
|
method: "PUT",
|
|
97
113
|
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
98
|
-
body: JSON.stringify(
|
|
99
|
-
|
|
114
|
+
body: JSON.stringify(payload),
|
|
115
|
+
signal: AbortSignal.timeout(5000),
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
let msg = `HTTP ${res.status}`;
|
|
119
|
+
try { const data = await res.json(); if (data?.error) msg = data.error; } catch {}
|
|
120
|
+
return { ok: false, error: msg };
|
|
121
|
+
}
|
|
122
|
+
return { ok: true };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return { ok: false, error: err?.message || "network error" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeLocalConfig(configPath, config) {
|
|
129
|
+
try {
|
|
130
|
+
const payload = stripCredentials(config);
|
|
131
|
+
writeFileSync(configPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`Warning: Failed to write .agentdesk.json: ${err.message}`);
|
|
100
134
|
}
|
|
135
|
+
}
|
|
101
136
|
|
|
102
|
-
|
|
137
|
+
function stripCredentials(config) {
|
|
138
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
139
|
+
for (const [section, field] of CREDENTIAL_PATHS) {
|
|
140
|
+
if (clone[section] && typeof clone[section] === "object") {
|
|
141
|
+
delete clone[section][field];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return clone;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Walk two configs and return every leaf where both sides have a non-null
|
|
148
|
+
// value and the values disagree. Credential paths are ignored — the server
|
|
149
|
+
// masks them to `••••••` in GET responses, so "conflict" there is spurious.
|
|
150
|
+
function findConflicts(local, server, prefix = "") {
|
|
151
|
+
const out = [];
|
|
152
|
+
if (local === null || server === null) return out;
|
|
153
|
+
const isObj = v => v && typeof v === "object" && !Array.isArray(v);
|
|
154
|
+
if (isObj(local) && isObj(server)) {
|
|
155
|
+
for (const key of Object.keys(local)) {
|
|
156
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
157
|
+
if (isCredentialPath(path)) continue;
|
|
158
|
+
if (!(key in server)) continue;
|
|
159
|
+
out.push(...findConflicts(local[key], server[key], path));
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
if (JSON.stringify(local) !== JSON.stringify(server)) {
|
|
164
|
+
out.push({ path: prefix, local, server });
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isCredentialPath(path) {
|
|
170
|
+
return CREDENTIAL_PATHS.some(([section, field]) => path === `${section}.${field}`);
|
|
103
171
|
}
|
|
104
172
|
|
|
105
173
|
function deepMerge(defaults, overrides) {
|