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.
- package/bin/_cli/add-cwd.mjs +190 -0
- package/bin/_cli/index.mjs +7 -0
- package/bin/_cli/protocol.mjs +46 -6
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/bin/_cli/index.mjs
CHANGED
|
@@ -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));
|
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|