@kendoo.agentdesk/agentdesk 0.15.5 → 0.16.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/CHANGELOG.md +13 -0
- package/bin/agentdesk.mjs +3 -2
- package/cli/config.mjs +116 -48
- package/cli/init.mjs +143 -13
- package/cli/tracker-check.mjs +27 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ 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.16.0] — 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `[CLI]` `agentdesk init` now walks you through tracker setup as a guided wizard — numbered steps, a side-by-side "dedicated AgentDesk user vs. your own account" recommendation with per-tracker instructions (Linear, Jira, GitHub), and a final review screen before anything is written.
|
|
15
|
+
- `[CLI]` After credentials verify, the wizard prints the identity your team will post under ("✓ Posting as: …"). If it looks like a personal account, a gentle nudge suggests rotating to a dedicated user for cleaner ticket attribution.
|
|
16
|
+
- `[CLI]` Re-running `agentdesk init` in a project that already has config offers a "tracker only" shortcut instead of walking the whole wizard.
|
|
17
|
+
- `[CLI]` `agentdesk init --quick` skips the narrative copy for power users and scripted setups.
|
|
18
|
+
|
|
19
|
+
## [0.15.6] — 2026-04-18
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `[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.
|
|
23
|
+
|
|
11
24
|
## [0.15.5] — 2026-04-18
|
|
12
25
|
|
|
13
26
|
### Added
|
package/bin/agentdesk.mjs
CHANGED
|
@@ -61,7 +61,7 @@ if (!command || command === "help" || command === "--help") {
|
|
|
61
61
|
Commands:
|
|
62
62
|
agentdesk login Sign in to AgentDesk
|
|
63
63
|
agentdesk logout Sign out and remove credentials
|
|
64
|
-
agentdesk init
|
|
64
|
+
agentdesk init [--quick] Set up project and configure tracker
|
|
65
65
|
agentdesk team <TASK-ID> Run a team session on an existing task
|
|
66
66
|
agentdesk team -d "..." Create a task and run a session
|
|
67
67
|
agentdesk <agent> -d "..." Run a single agent (jane, dennis, sam, bart, vera, luna, mark, nora)
|
|
@@ -142,7 +142,8 @@ else if (command === "logout") {
|
|
|
142
142
|
|
|
143
143
|
else if (command === "init") {
|
|
144
144
|
const { runInit } = await import("../cli/init.mjs");
|
|
145
|
-
|
|
145
|
+
const quick = args.slice(1).includes("--quick");
|
|
146
|
+
await runInit(process.cwd(), { quick });
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
else if (command === "team") {
|
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) {
|
package/cli/init.mjs
CHANGED
|
@@ -1,16 +1,94 @@
|
|
|
1
|
-
// `agentdesk init` — interactive project setup with tracker configuration
|
|
1
|
+
// `agentdesk init` — interactive project setup with tracker configuration.
|
|
2
|
+
//
|
|
3
|
+
// Run modes:
|
|
4
|
+
// agentdesk init → full wizard with inline guidance
|
|
5
|
+
// agentdesk init --quick → skip narrative copy for power users / re-runs
|
|
6
|
+
// (automatic) re-run → when .agentdesk.json already exists, offer
|
|
7
|
+
// to edit only a specific section
|
|
2
8
|
|
|
3
9
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
10
|
import { join } from "path";
|
|
5
11
|
import { createInterface } from "readline";
|
|
6
12
|
import { detectProject } from "./detect.mjs";
|
|
7
|
-
import { loadConfig } from "./config.mjs";
|
|
13
|
+
import { loadConfig, pushConfig } from "./config.mjs";
|
|
8
14
|
import { getStoredApiKey } from "./login.mjs";
|
|
9
15
|
import { registerLocalProject } from "./projects.mjs";
|
|
10
16
|
import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
|
|
11
17
|
|
|
12
18
|
const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
|
|
13
19
|
|
|
20
|
+
// Pretty dividers and tracker-specific "dedicated user" guidance.
|
|
21
|
+
const TRACKER_GUIDANCE = {
|
|
22
|
+
linear: {
|
|
23
|
+
name: "Linear",
|
|
24
|
+
dedicatedSteps: [
|
|
25
|
+
"1. Open https://linear.app/<your-workspace>/settings/members",
|
|
26
|
+
"2. Invite a new member named \"AgentDesk\" to an email you control",
|
|
27
|
+
"3. Accept the invite from that email, log in as the new user",
|
|
28
|
+
"4. Go to https://linear.app/settings/api → Create key → label it \"AgentDesk\"",
|
|
29
|
+
"5. Paste the key when prompted below",
|
|
30
|
+
],
|
|
31
|
+
personalNote: "Your own Linear account. Comments on tickets will appear under your name.",
|
|
32
|
+
},
|
|
33
|
+
jira: {
|
|
34
|
+
name: "Jira",
|
|
35
|
+
dedicatedSteps: [
|
|
36
|
+
"1. Open https://admin.atlassian.com/ → Directory → Invite users",
|
|
37
|
+
"2. Invite an email you control (e.g. agentdesk@yourdomain.com) as \"AgentDesk\"",
|
|
38
|
+
"3. Accept the invite and sign in as the new user",
|
|
39
|
+
"4. Go to https://id.atlassian.com/manage-profile/security/api-tokens → Create API token",
|
|
40
|
+
"5. Paste the email + token when prompted below",
|
|
41
|
+
],
|
|
42
|
+
personalNote: "Your own Jira account. Comments on tickets will appear under your name.",
|
|
43
|
+
},
|
|
44
|
+
github: {
|
|
45
|
+
name: "GitHub",
|
|
46
|
+
dedicatedSteps: [
|
|
47
|
+
"GitHub enforces one account per human, so a \"dedicated bot user\" must be",
|
|
48
|
+
"a GitHub App (first-class bot identity, shows as agentdesk[bot] on comments).",
|
|
49
|
+
"That's a deeper install — not available yet in this wizard. For now:",
|
|
50
|
+
" • Option A: Use gh CLI authenticated as yourself (quickest to get started)",
|
|
51
|
+
" • Option B: Generate a Personal Access Token scoped to the repo",
|
|
52
|
+
],
|
|
53
|
+
personalNote: "Your own GitHub account via gh CLI or a PAT. Comments appear under your name.",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function printDedicatedUserChoice(tracker, quick) {
|
|
58
|
+
if (quick || !tracker) return;
|
|
59
|
+
const g = TRACKER_GUIDANCE[tracker];
|
|
60
|
+
if (!g) return;
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log(` ${g.name} account — who will post the comments?`);
|
|
63
|
+
console.log(" ─────────────────────────────────────────────");
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(" Recommended: Dedicated \"AgentDesk\" user");
|
|
66
|
+
console.log(" Clean audit trail, survives team changes, clear attribution.");
|
|
67
|
+
for (const line of g.dedicatedSteps) console.log(` ${line}`);
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(" Quick start: Your own account");
|
|
70
|
+
console.log(` ${g.personalNote}`);
|
|
71
|
+
console.log("");
|
|
72
|
+
console.log(" (This wizard doesn't care which you pick — it only asks for the");
|
|
73
|
+
console.log(" credential. You're deciding whose credential to paste.)");
|
|
74
|
+
console.log("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printIdentityEcho(identity) {
|
|
78
|
+
if (!identity) return;
|
|
79
|
+
const label = identity.name || identity.login || identity.email || "(unknown)";
|
|
80
|
+
const extra = identity.email && identity.email !== identity.name ? ` <${identity.email}>` : "";
|
|
81
|
+
console.log(` ✓ Posting as: ${label}${extra}`);
|
|
82
|
+
const looksLikePerson =
|
|
83
|
+
identity.name && !/agent\s*desk|agentdesk|bot|service/i.test(identity.name) &&
|
|
84
|
+
!/(^|[.+])(agentdesk|bot|svc|service)(@|$|[.+])/i.test(identity.email || "");
|
|
85
|
+
if (looksLikePerson) {
|
|
86
|
+
console.log(" (Looks like a personal account — comments will be attributed to this user.");
|
|
87
|
+
console.log(" If you'd rather have a dedicated AgentDesk identity, rotate the credential");
|
|
88
|
+
console.log(" to one generated under a dedicated tracker user and re-run `agentdesk init`.)");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
14
92
|
function loadApiKey(dir) {
|
|
15
93
|
const envPath = join(dir, ".env");
|
|
16
94
|
if (!existsSync(envPath)) return getStoredApiKey();
|
|
@@ -76,7 +154,8 @@ async function fetchServerCreds(apiKey, projectKey) {
|
|
|
76
154
|
return {};
|
|
77
155
|
}
|
|
78
156
|
|
|
79
|
-
export async function runInit(cwd) {
|
|
157
|
+
export async function runInit(cwd, opts = {}) {
|
|
158
|
+
const quick = !!opts.quick;
|
|
80
159
|
const project = detectProject(cwd);
|
|
81
160
|
const existingConfig = loadConfig(cwd);
|
|
82
161
|
const projectId = project.name || cwd.split("/").pop();
|
|
@@ -85,6 +164,26 @@ export async function runInit(cwd) {
|
|
|
85
164
|
|
|
86
165
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
87
166
|
|
|
167
|
+
// Re-run mode: if config already exists (and we're not in --quick), let
|
|
168
|
+
// the user jump to a specific section instead of walking the full wizard.
|
|
169
|
+
let editScope = "full";
|
|
170
|
+
if (hasConfig && !quick) {
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log(" AgentDesk — existing project detected");
|
|
173
|
+
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
174
|
+
console.log("");
|
|
175
|
+
const choice = await selectOption(rl, "What would you like to do?", [
|
|
176
|
+
{ label: "Full setup (walk the whole wizard again)", value: "full" },
|
|
177
|
+
{ label: "Tracker only (change tracker or credentials)", value: "tracker" },
|
|
178
|
+
{ label: "Cancel", value: "cancel" },
|
|
179
|
+
]);
|
|
180
|
+
if (choice.value === "cancel") {
|
|
181
|
+
rl.close();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
editScope = choice.value;
|
|
185
|
+
}
|
|
186
|
+
|
|
88
187
|
console.log("");
|
|
89
188
|
console.log(" AgentDesk — Project Setup");
|
|
90
189
|
console.log(" ━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
@@ -103,11 +202,20 @@ export async function runInit(cwd) {
|
|
|
103
202
|
|
|
104
203
|
// --- Step 1: Project key ---
|
|
105
204
|
const defaultKey = existingConfig.projectKey || projectId;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
205
|
+
let finalProjectKey;
|
|
206
|
+
if (editScope === "tracker") {
|
|
207
|
+
finalProjectKey = defaultKey;
|
|
208
|
+
} else {
|
|
209
|
+
console.log(" Step 1 of 4 — Project key");
|
|
210
|
+
console.log("");
|
|
211
|
+
const keyAnswer = await ask(rl, ` Project key (${defaultKey}): `);
|
|
212
|
+
finalProjectKey = keyAnswer.trim() || defaultKey;
|
|
213
|
+
console.log("");
|
|
214
|
+
}
|
|
109
215
|
|
|
110
216
|
// --- Step 2: Tracker selection (platform first) ---
|
|
217
|
+
if (!quick) console.log(editScope === "tracker" ? " Tracker setup" : " Step 2 of 4 — Task tracker");
|
|
218
|
+
console.log("");
|
|
111
219
|
const trackerOptions = [
|
|
112
220
|
{ label: "Linear", value: "linear" },
|
|
113
221
|
{ label: "Jira", value: "jira" },
|
|
@@ -119,6 +227,9 @@ export async function runInit(cwd) {
|
|
|
119
227
|
const tracker = selected.value;
|
|
120
228
|
console.log("");
|
|
121
229
|
|
|
230
|
+
// Inline guidance: dedicated-user vs personal, only when a tracker is picked.
|
|
231
|
+
printDedicatedUserChoice(tracker, quick);
|
|
232
|
+
|
|
122
233
|
// --- Step 2: Tracker configuration (details + credentials) ---
|
|
123
234
|
const config = {};
|
|
124
235
|
if (tracker) config.tracker = tracker;
|
|
@@ -166,6 +277,7 @@ export async function runInit(cwd) {
|
|
|
166
277
|
|
|
167
278
|
if (check.ok) {
|
|
168
279
|
console.log(" ✓ Tracker permissions verified (read, create, update)");
|
|
280
|
+
printIdentityEcho(check.identity);
|
|
169
281
|
console.log("");
|
|
170
282
|
verified = true;
|
|
171
283
|
} else {
|
|
@@ -329,7 +441,7 @@ export async function runInit(cwd) {
|
|
|
329
441
|
}
|
|
330
442
|
}
|
|
331
443
|
|
|
332
|
-
// ---
|
|
444
|
+
// --- Summary + confirm before write ---
|
|
333
445
|
let merged = {};
|
|
334
446
|
if (hasConfig) {
|
|
335
447
|
try { merged = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
|
|
@@ -349,6 +461,24 @@ export async function runInit(cwd) {
|
|
|
349
461
|
delete merged.github;
|
|
350
462
|
}
|
|
351
463
|
|
|
464
|
+
if (!quick) {
|
|
465
|
+
console.log(" Review");
|
|
466
|
+
console.log(" ──────");
|
|
467
|
+
console.log(` Project key: ${merged.projectKey}`);
|
|
468
|
+
console.log(` Tracker: ${merged.tracker || "none"}`);
|
|
469
|
+
if (merged.linear) console.log(` Linear: workspace=${merged.linear.workspace || "-"}, team=${merged.linear.teamKey || "-"}`);
|
|
470
|
+
if (merged.jira) console.log(` Jira: ${merged.jira.baseUrl || "-"}, project=${merged.jira.project || "-"}`);
|
|
471
|
+
if (merged.github) console.log(` GitHub: ${merged.github.repo || "-"}`);
|
|
472
|
+
console.log("");
|
|
473
|
+
const confirm = (await ask(rl, " Save this config? [Y/n]: ")).trim().toLowerCase();
|
|
474
|
+
if (confirm && confirm !== "y" && confirm !== "yes") {
|
|
475
|
+
console.log(" Cancelled — nothing written.");
|
|
476
|
+
rl.close();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
console.log("");
|
|
480
|
+
}
|
|
481
|
+
|
|
352
482
|
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n");
|
|
353
483
|
console.log(` Saved .agentdesk.json`);
|
|
354
484
|
|
|
@@ -385,14 +515,14 @@ export async function runInit(cwd) {
|
|
|
385
515
|
if (res.ok) {
|
|
386
516
|
console.log(" Registered with agentdesk.live");
|
|
387
517
|
}
|
|
388
|
-
// Push settings to server
|
|
518
|
+
// Push settings to server via the shared helper so error surfacing is consistent.
|
|
389
519
|
const key = loadApiKey(cwd);
|
|
390
520
|
if (key) {
|
|
391
|
-
await
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
521
|
+
const push = await pushConfig(key, SERVER, finalProjectKey, merged);
|
|
522
|
+
if (!push.ok && push.error !== "missing_auth") {
|
|
523
|
+
console.log(` ⚠ Could not push settings to agentdesk.live: ${push.error}`);
|
|
524
|
+
console.log(" Your local .agentdesk.json is saved; retry from the UI or re-run init.");
|
|
525
|
+
}
|
|
396
526
|
}
|
|
397
527
|
} catch {
|
|
398
528
|
// Server not available
|
package/cli/tracker-check.mjs
CHANGED
|
@@ -29,13 +29,16 @@ async function checkLinear({ teamKey, workspace }, creds) {
|
|
|
29
29
|
if (!apiKey) return { ok: false, errors: ["Missing LINEAR_API_KEY — configure it in the AgentDesk dashboard or .env"] };
|
|
30
30
|
|
|
31
31
|
const errors = [];
|
|
32
|
+
let identity = null;
|
|
32
33
|
|
|
33
34
|
// Check read: fetch viewer and team info
|
|
34
35
|
try {
|
|
35
|
-
const viewerRes = await gql(apiKey, `{ viewer { id name } }`);
|
|
36
|
+
const viewerRes = await gql(apiKey, `{ viewer { id name email } }`);
|
|
36
37
|
if (viewerRes.errors) {
|
|
37
38
|
return { ok: false, errors: ["LINEAR_API_KEY is invalid or expired"] };
|
|
38
39
|
}
|
|
40
|
+
const v = viewerRes.data?.viewer;
|
|
41
|
+
if (v) identity = { name: v.name, email: v.email, id: v.id };
|
|
39
42
|
} catch (e) {
|
|
40
43
|
return { ok: false, errors: [`Cannot reach Linear API: ${e.message}`] };
|
|
41
44
|
}
|
|
@@ -77,7 +80,7 @@ async function checkLinear({ teamKey, workspace }, creds) {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
return { ok: errors.length === 0, errors };
|
|
83
|
+
return { ok: errors.length === 0, errors, identity };
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
async function checkJira({ baseUrl, project }, creds) {
|
|
@@ -88,8 +91,21 @@ async function checkJira({ baseUrl, project }, creds) {
|
|
|
88
91
|
if (!baseUrl) return { ok: false, errors: ["Missing Jira base URL — run 'agentdesk init' to configure"] };
|
|
89
92
|
|
|
90
93
|
const errors = [];
|
|
94
|
+
let identity = null;
|
|
91
95
|
const auth = "Basic " + Buffer.from(`${email}:${token}`).toString("base64");
|
|
92
96
|
|
|
97
|
+
// Fetch the authenticated user so we can report "Posting as X".
|
|
98
|
+
try {
|
|
99
|
+
const meRes = await fetch(`${baseUrl}/rest/api/3/myself`, {
|
|
100
|
+
headers: { Authorization: auth, Accept: "application/json" },
|
|
101
|
+
signal: AbortSignal.timeout(10000),
|
|
102
|
+
});
|
|
103
|
+
if (meRes.ok) {
|
|
104
|
+
const me = await meRes.json();
|
|
105
|
+
identity = { name: me.displayName || email, email: me.emailAddress || email, id: me.accountId };
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
93
109
|
// Use Jira's mypermissions endpoint to check all permissions at once
|
|
94
110
|
const permissionsToCheck = "BROWSE_PROJECTS,CREATE_ISSUES,EDIT_ISSUES,ADD_COMMENTS,TRANSITION_ISSUES";
|
|
95
111
|
try {
|
|
@@ -133,13 +149,14 @@ async function checkJira({ baseUrl, project }, creds) {
|
|
|
133
149
|
return { ok: false, errors: [`Cannot reach Jira at ${baseUrl}: ${e.message}`] };
|
|
134
150
|
}
|
|
135
151
|
|
|
136
|
-
return { ok: errors.length === 0, errors };
|
|
152
|
+
return { ok: errors.length === 0, errors, identity };
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
async function checkGitHub({ repo }, creds) {
|
|
140
156
|
if (!repo) return { ok: false, errors: ["Missing GitHub repo — run 'agentdesk init' to configure"] };
|
|
141
157
|
|
|
142
158
|
const errors = [];
|
|
159
|
+
let identity = null;
|
|
143
160
|
|
|
144
161
|
// Check if gh CLI is available
|
|
145
162
|
try {
|
|
@@ -148,9 +165,14 @@ async function checkGitHub({ repo }, creds) {
|
|
|
148
165
|
return { ok: false, errors: ["GitHub CLI (gh) is not installed — install it from https://cli.github.com"] };
|
|
149
166
|
}
|
|
150
167
|
|
|
151
|
-
// Check auth status
|
|
168
|
+
// Check auth status and grab the authenticated login
|
|
152
169
|
try {
|
|
153
170
|
execSync("gh auth status", { stdio: "pipe" });
|
|
171
|
+
try {
|
|
172
|
+
const who = execSync(`gh api user --jq '{login: .login, name: .name}'`, { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
173
|
+
const j = JSON.parse(who);
|
|
174
|
+
identity = { name: j.name || j.login, login: j.login };
|
|
175
|
+
} catch {}
|
|
154
176
|
} catch {
|
|
155
177
|
return { ok: false, errors: ["GitHub CLI is not authenticated — run 'gh auth login'"] };
|
|
156
178
|
}
|
|
@@ -175,7 +197,7 @@ async function checkGitHub({ repo }, creds) {
|
|
|
175
197
|
}
|
|
176
198
|
}
|
|
177
199
|
|
|
178
|
-
return { ok: errors.length === 0, errors };
|
|
200
|
+
return { ok: errors.length === 0, errors, identity };
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
// Helper: execute a Linear GraphQL query
|