overlord-cli 0.2605221356.0 → 0.2605230834.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.
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ // `ovld add-cwd` — interactively add the current working directory as a
4
+ // project resource. Lists the user's projects, prompts for a selection,
5
+ // registers cwd as a primary resource for this device, and writes the
6
+ // project entry into the directory's overlord.json file.
7
+
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import readline from 'node:readline';
11
+ import { stdin as input, stdout as output } from 'node:process';
12
+
13
+ import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
14
+ import { upsertLocalOverlordConfig } from './local-config.mjs';
15
+ import { parseNumberedSelection, sortProjects } from './new-ticket.mjs';
16
+ import { readOrCreateDeviceFingerprint } from './runner.mjs';
17
+
18
+ function parseFlags(args) {
19
+ const flags = {};
20
+ for (let i = 0; i < args.length; i++) {
21
+ const arg = args[i];
22
+ if (!arg.startsWith('--')) continue;
23
+ const eqIdx = arg.indexOf('=');
24
+ if (eqIdx !== -1) {
25
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
26
+ continue;
27
+ }
28
+ const key = arg.slice(2);
29
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
30
+ flags[key] = args[i + 1];
31
+ i++;
32
+ } else {
33
+ flags[key] = true;
34
+ }
35
+ }
36
+ return flags;
37
+ }
38
+
39
+ function projectLabel(project) {
40
+ const organizationName = String(project.organizationName ?? '').trim();
41
+ return organizationName ? `${project.name} — ${organizationName}` : project.name;
42
+ }
43
+
44
+ async function fetchProjects(platformUrl, bearerToken, localSecret, organizationId) {
45
+ const res = await fetch(`${platformUrl}/api/protocol/projects`, {
46
+ headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
47
+ });
48
+ const data = await res.json().catch(() => ({}));
49
+ if (!res.ok) {
50
+ throw new Error(
51
+ `Failed to list projects (${res.status}): ${data.error ?? JSON.stringify(data)}`
52
+ );
53
+ }
54
+ return Array.isArray(data.projects) ? sortProjects(data.projects) : [];
55
+ }
56
+
57
+ async function promptForProject(projects, cwd) {
58
+ if (!projects.length) {
59
+ throw new Error('No projects available. Create a project in Overlord first.');
60
+ }
61
+
62
+ const rl = readline.createInterface({ input, output });
63
+ const ask = q =>
64
+ new Promise((resolve, reject) => {
65
+ const onError = error => {
66
+ rl.off('error', onError);
67
+ reject(error);
68
+ };
69
+ rl.once('error', onError);
70
+ rl.question(q, answer => {
71
+ rl.off('error', onError);
72
+ resolve(answer);
73
+ });
74
+ });
75
+
76
+ try {
77
+ while (true) {
78
+ output.write(`\nCurrent directory:\n ${cwd}\n\nProjects:\n`);
79
+ projects.forEach((p, i) => {
80
+ output.write(` ${i + 1}. ${projectLabel(p)}\n`);
81
+ });
82
+ const answer = await ask(`\nSelect a project to register this directory with (1-${projects.length}, or 'q' to cancel): `);
83
+ if (answer.trim().toLowerCase() === 'q') {
84
+ return null;
85
+ }
86
+ const idx = parseNumberedSelection(answer, projects.length);
87
+ if (idx !== null) return projects[idx];
88
+ output.write(`Enter a number between 1 and ${projects.length}.\n`);
89
+ }
90
+ } finally {
91
+ rl.close();
92
+ }
93
+ }
94
+
95
+ export async function runAddCwdCommand(args) {
96
+ const flags = parseFlags(args);
97
+
98
+ if (flags.help === true || flags.h === true) {
99
+ console.log(`Usage: ovld add-cwd [--directory <path>] [--project-id <id>] [--primary]
100
+
101
+ Registers a directory (defaulting to the current working directory) as a
102
+ project resource for this device, and writes the project entry into the
103
+ directory's overlord.json file.
104
+
105
+ When --project-id is omitted, the command lists your projects and prompts
106
+ you to pick one.
107
+
108
+ By default the registered resource is marked primary for this device. Pass
109
+ --primary=false to skip that behaviour.`);
110
+ return;
111
+ }
112
+
113
+ const directoryPath = path.resolve(
114
+ typeof flags.directory === 'string' && flags.directory.trim()
115
+ ? flags.directory.trim()
116
+ : process.cwd()
117
+ );
118
+
119
+ const isPrimary = !(flags.primary === 'false' || flags.primary === false);
120
+
121
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth({});
122
+
123
+ const projects = await fetchProjects(platformUrl, bearerToken, localSecret, organizationId);
124
+ let project = null;
125
+ if (typeof flags['project-id'] === 'string' && flags['project-id'].trim()) {
126
+ const wanted = flags['project-id'].trim();
127
+ project = projects.find(p => p.id === wanted) ?? null;
128
+ if (!project) {
129
+ console.error(`Error: project ${wanted} not found among your projects.`);
130
+ process.exit(1);
131
+ }
132
+ } else {
133
+ project = await promptForProject(projects, directoryPath);
134
+ if (!project) {
135
+ output.write('Cancelled. No changes made.\n');
136
+ return;
137
+ }
138
+ }
139
+
140
+ const deviceFingerprint = readOrCreateDeviceFingerprint(flags);
141
+
142
+ const body = {
143
+ projectId: project.id,
144
+ directoryPath,
145
+ deviceFingerprint,
146
+ isPrimary,
147
+ deviceHostname: os.hostname(),
148
+ devicePlatform: process.platform
149
+ };
150
+
151
+ const res = await fetch(`${platformUrl}/api/protocol/add-project-resource`, {
152
+ method: 'POST',
153
+ headers: {
154
+ ...buildAuthHeaders(bearerToken, localSecret, organizationId),
155
+ 'Content-Type': 'application/json'
156
+ },
157
+ body: JSON.stringify(body)
158
+ });
159
+ const data = await res.json().catch(() => ({}));
160
+ if (!res.ok) {
161
+ if (res.status === 409) {
162
+ output.write(
163
+ `\nDirectory is already registered for "${project.name}" on this device. Updating overlord.json only.\n`
164
+ );
165
+ } else {
166
+ console.error(
167
+ `Failed to register resource (${res.status}): ${data.error ?? JSON.stringify(data)}`
168
+ );
169
+ process.exit(1);
170
+ }
171
+ }
172
+
173
+ try {
174
+ const result = await upsertLocalOverlordConfig({
175
+ directoryPath,
176
+ project: { id: project.id, name: project.name }
177
+ });
178
+ output.write(`\nWrote ${result.filePath} (${result.action}).\n`);
179
+ } catch (err) {
180
+ output.write(
181
+ `\nWarning: could not update overlord.json: ${err instanceof Error ? err.message : err}\n`
182
+ );
183
+ }
184
+
185
+ if (data.resource?.id) {
186
+ output.write(`Registered resource ${data.resource.id} (primary=${data.resource.isPrimary}).\n`);
187
+ } else if (res.ok) {
188
+ output.write(`Registered ${directoryPath} for project "${project.name}".\n`);
189
+ }
190
+ }
@@ -32,6 +32,7 @@ Usage:
32
32
  ${primaryCommand} attach [ticketId] [agent] Search tickets and launch an agent (interactive)
33
33
  ${primaryCommand} create "<objective>" Create a ticket with numbered project selection; supports --agent/--model/--delegate
34
34
  ${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
35
+ ${primaryCommand} add-cwd Register the current directory as a project resource (interactive)
35
36
  ${primaryCommand} auth <subcommand> Login, logout, repair, or check auth status
36
37
  ${primaryCommand} tickets <subcommand> Create or list tickets
37
38
  ${primaryCommand} ticket <subcommand> Work with a single ticket
@@ -105,6 +106,12 @@ export async function runCli({ primaryCommand }) {
105
106
  return;
106
107
  }
107
108
 
109
+ if (command === 'add-cwd') {
110
+ const { runAddCwdCommand } = await import('./add-cwd.mjs');
111
+ await runAddCwdCommand(rest);
112
+ return;
113
+ }
114
+
108
115
  // Auth group
109
116
  if (command === 'auth') {
110
117
  await runAuthCommand(rest[0], rest.slice(1));
@@ -212,12 +212,49 @@ async function uploadToSignedUrl(uploadUrl, bytes, contentType, timeoutMs = DEFA
212
212
  }
213
213
  }
214
214
 
215
+ // ---------------------------------------------------------------------------
216
+ // Session file persistence — auto-resolve session key between CLI calls
217
+ // ---------------------------------------------------------------------------
218
+
219
+ function getSessionFilePath() {
220
+ return path.join(os.tmpdir(), `.overlord-session-${Buffer.from(process.cwd()).toString('base64url')}`);
221
+ }
222
+
223
+ function persistSession(sessionKey, ticketId) {
224
+ try {
225
+ fs.writeFileSync(
226
+ getSessionFilePath(),
227
+ JSON.stringify({ sessionKey, ticketId, ts: Date.now() }),
228
+ 'utf8'
229
+ );
230
+ } catch { /* best-effort */ }
231
+ }
232
+
233
+ function readPersistedSession() {
234
+ try {
235
+ const raw = fs.readFileSync(getSessionFilePath(), 'utf8');
236
+ const data = JSON.parse(raw);
237
+ if (data.sessionKey && data.ticketId) return data;
238
+ } catch { /* missing or corrupt — ignore */ }
239
+ return null;
240
+ }
241
+
215
242
  /** Read SESSION_KEY and TICKET_ID from env if flags not provided */
216
243
  function resolveSessionFlags(flags) {
217
- return {
218
- sessionKey: String(flags['session-key'] ?? process.env.SESSION_KEY ?? ''),
219
- ticketId: String(flags['ticket-id'] ?? process.env.TICKET_ID ?? '')
220
- };
244
+ const sessionKey = String(flags['session-key'] ?? process.env.SESSION_KEY ?? '');
245
+ const ticketId = String(flags['ticket-id'] ?? process.env.TICKET_ID ?? '');
246
+
247
+ if (sessionKey && ticketId) return { sessionKey, ticketId };
248
+
249
+ const persisted = readPersistedSession();
250
+ if (persisted) {
251
+ return {
252
+ sessionKey: sessionKey || persisted.sessionKey,
253
+ ticketId: ticketId || persisted.ticketId
254
+ };
255
+ }
256
+
257
+ return { sessionKey, ticketId };
221
258
  }
222
259
 
223
260
  /** Resolve request timeout from --timeout flag or OVERLORD_TIMEOUT env var. */
@@ -944,6 +981,7 @@ async function protocolAttach(args) {
944
981
  console.log(JSON.stringify(data, null, 2));
945
982
 
946
983
  if (sessionKey) {
984
+ persistSession(sessionKey, ticketId);
947
985
  // Emit a machine-readable line for easy shell capture:
948
986
  // SESSION_KEY=$(ovld protocol attach --ticket-id ... | grep ^SESSION_KEY= | cut -d= -f2)
949
987
  process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
@@ -2026,6 +2064,7 @@ async function protocolConnect(args) {
2026
2064
  console.log(JSON.stringify(data, null, 2));
2027
2065
 
2028
2066
  if (sessionKey) {
2067
+ persistSession(sessionKey, ticketId);
2029
2068
  process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
2030
2069
  }
2031
2070
  }
@@ -2152,6 +2191,7 @@ async function protocolPrompt(args) {
2152
2191
  console.log(JSON.stringify(data, null, 2));
2153
2192
 
2154
2193
  if (sessionKey) {
2194
+ if (ticketId) persistSession(sessionKey, ticketId);
2155
2195
  process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
2156
2196
  }
2157
2197
  if (ticketId) {
@@ -2544,8 +2584,8 @@ Subcommands:
2544
2584
  update-project-resource Update a resource directory's path, label, or primary status
2545
2585
 
2546
2586
  Environment fallback:
2547
- --session-key <- SESSION_KEY
2548
- --ticket-id <- TICKET_ID (human-readable ticket_id, e.g. 1:899)
2587
+ --session-key <- SESSION_KEY or auto-persisted session from last attach/connect/prompt in this working directory
2588
+ --ticket-id <- TICKET_ID or auto-persisted session (human-readable ticket_id, e.g. 1:899)
2549
2589
  auth/host <- OVERLORD_URL, optional OVERLORD_ACCESS_TOKEN + OVERLORD_ORGANIZATION_ID, or shared OAuth credentials from ovld auth/Desktop login
2550
2590
  --timeout <- OVERLORD_TIMEOUT
2551
2591
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605221356.0",
3
+ "version": "0.2605230834.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",