overlord-cli 0.2605211850.0 → 0.2605211850.1

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.
@@ -9,6 +9,12 @@ import path from 'node:path';
9
9
  const OVLD_ENTRY = process.argv[1];
10
10
  const DEVICE_FILE = path.join(os.homedir(), '.ovld', 'device.json');
11
11
 
12
+ /** @internal Test-only overrides for protocol/launch subprocess calls. */
13
+ export const runnerTestHooks = {
14
+ execFileSync: null,
15
+ spawn: null
16
+ };
17
+
12
18
  function printRunnerHelp(primaryCommand = 'ovld') {
13
19
  console.log(`Usage:
14
20
  ${primaryCommand} runner once [options]
@@ -45,7 +51,7 @@ function parseFlags(args) {
45
51
  return flags;
46
52
  }
47
53
 
48
- function readOrCreateDeviceFingerprint(flags) {
54
+ export function readOrCreateDeviceFingerprint(flags) {
49
55
  const explicit =
50
56
  typeof flags['device-fingerprint'] === 'string'
51
57
  ? flags['device-fingerprint'].trim()
@@ -69,7 +75,8 @@ function readOrCreateDeviceFingerprint(flags) {
69
75
  }
70
76
 
71
77
  function runOvld(args, env = {}) {
72
- return execFileSync(process.execPath, [OVLD_ENTRY, ...args], {
78
+ const exec = runnerTestHooks.execFileSync ?? execFileSync;
79
+ return exec(process.execPath, [OVLD_ENTRY, ...args], {
73
80
  encoding: 'utf8',
74
81
  env: { ...process.env, ...env }
75
82
  });
@@ -111,7 +118,7 @@ function failLaunch(requestId, deviceFingerprint, error) {
111
118
  );
112
119
  }
113
120
 
114
- function buildLaunchArgs(launch) {
121
+ export function buildLaunchArgs(launch) {
115
122
  const args = ['launch', launch.agent, '--ticket-id', launch.ticketId];
116
123
  if (launch.workingDirectory) args.push('--working-directory', launch.workingDirectory);
117
124
  if (launch.launchMode === 'ask') args.push('--launch-mode', 'ask');
@@ -127,7 +134,7 @@ function buildLaunchArgs(launch) {
127
134
  return args;
128
135
  }
129
136
 
130
- async function launchClaimedRequest(claim, deviceFingerprint) {
137
+ export async function launchClaimedRequest(claim, deviceFingerprint) {
131
138
  const requestId = claim.request?.id;
132
139
  if (!requestId || !claim.launch?.agent || !claim.launch?.ticketId) return false;
133
140
 
@@ -137,7 +144,8 @@ async function launchClaimedRequest(claim, deviceFingerprint) {
137
144
  );
138
145
 
139
146
  await new Promise((resolve, reject) => {
140
- const child = spawn(process.execPath, [OVLD_ENTRY, ...args], {
147
+ const spawnImpl = runnerTestHooks.spawn ?? spawn;
148
+ const child = spawnImpl(process.execPath, [OVLD_ENTRY, ...args], {
141
149
  stdio: 'inherit',
142
150
  env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
143
151
  });
@@ -170,7 +178,7 @@ async function launchClaimedRequest(claim, deviceFingerprint) {
170
178
  return true;
171
179
  }
172
180
 
173
- async function runOnce(flags, deviceFingerprint) {
181
+ export async function runOnce(flags, deviceFingerprint) {
174
182
  const claim = claimExecution(flags, deviceFingerprint);
175
183
  if (!claim.request) {
176
184
  process.stderr.write('[runner] no queued execution requests\n');
@@ -14,10 +14,86 @@ import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { checkForCliUpdate, getCurrentCliVersion, printCliUpdateNotice } from './cli-update.mjs';
17
- import {
18
- geminiLegacyCommandFiles,
19
- removeLegacyGeminiConnector as removeLegacyGeminiConnectorFiles
20
- } from '../../../../lib/overlord/legacy-gemini-connector.cjs';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Legacy Gemini connector (inlined — the lib/ directory is not published with
20
+ // the npm package, so the old relative import broke on global installs)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const GEMINI_LEGACY_COMMANDS_DIR = path.join(os.homedir(), '.gemini', 'commands');
24
+
25
+ function geminiLegacyCommandFiles() {
26
+ const base = GEMINI_LEGACY_COMMANDS_DIR;
27
+ return [
28
+ {
29
+ path: path.join(base, 'connect.toml'),
30
+ content: `description = "Connect this session to another Overlord ticket (requires: ticket-id)."\nprompt = """\nConnect this session to another Overlord ticket.\n\nTreat \`{{args}}\` as the target ticket ID.\nIf no ticket ID was provided, ask the user for one and stop.\n\nRun:\n\`ovld protocol connect --ticket-id <ticket_id>\`\n\nRules:\n- Use \`connect\`, not \`attach\`.\n- Do not load extra ticket context unless the user explicitly asks for it.\n- After the command succeeds, report the returned \`SESSION_KEY\` and confirm that future updates should use that ticket.\n"""`
31
+ },
32
+ {
33
+ path: path.join(base, 'load.toml'),
34
+ content: `description = "Load Overlord ticket context (requires: ticket-id)."\nprompt = """\nLoad Overlord ticket context without attaching to the ticket.\n\nTreat \`{{args}}\` as the target ticket ID.\nIf no ticket ID was provided, ask the user for one and stop.\n\nRun:\n\`ovld protocol load-context --ticket-id <ticket_id>\`\n\nRules:\n- Use \`load-context\`, not \`attach\`.\n- Do not create or switch sessions.\n- Summarize the returned ticket details, history, artifacts, and shared context for the user.\n"""`
35
+ },
36
+ {
37
+ path: path.join(base, 'attach.toml'),
38
+ content: `description = "Attach this session to an Overlord ticket (requires: ticket-id)."\nprompt = """\nAttach this session to an Overlord ticket.\n\nTreat \`{{args}}\` as the target ticket ID.\nIf no ticket ID was provided, ask the user for one and stop.\n\nRun:\n\`ovld protocol attach --ticket-id <ticket_id>\`\n\nRules:\n- Use \`attach\` to establish a persistent session with a ticket.\n- After the command succeeds, report the returned \`SESSION_KEY\` and confirm that future updates should use that ticket.\n"""`
39
+ },
40
+ {
41
+ path: path.join(base, 'discuss-objective.toml'),
42
+ content: `description = "Mark a ticket's draft objective as submitted (in discussion)."\nprompt = """\nMark a draft objective as "submitted", indicating the ticket is in active discussion with an agent.\n\nTreat \`{{args}}\` as the target ticket ID.\nIf no ticket ID was provided, ask the user for one and stop.\n\nRun:\n\`ovld protocol discuss-objective --ticket-id <ticket_id>\`\n\nRules:\n- This does NOT start execution. Use \`attach\` for that.\n- After the command succeeds, confirm the objective was submitted.\n"""`
43
+ },
44
+ {
45
+ path: path.join(base, 'add-objectives.toml'),
46
+ content: `description = "Append ordered objectives to an existing Overlord ticket."\nprompt = """\nAppend ordered objectives to an existing ticket.\n\nUse this when prompts are sequential steps toward the same feature or goal. Create separate tickets when prompts represent different features or goals.\n\nRun:\n\`ovld protocol add-objectives --ticket-id <ticket_id> --objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'\`\n\nIndex 0 is the first newly added objective to execute; later indexes queue after it.\n"""`
47
+ },
48
+ {
49
+ path: path.join(base, 'create.toml'),
50
+ content: `description = "Create a draft Overlord ticket from the current conversation."\nprompt = """\nCreate a draft Overlord ticket from the user's request.\n\nUse \`{{args}}\` as the input.\nIf it already contains flags such as \`--title\`, \`--priority\`, \`--project-id\`, or \`--execution-target\`, pass those flags through after \`ovld protocol create --agent gemini\`.\nOtherwise, treat \`{{args}}\` as the objective text and run:\n\`ovld protocol create --agent gemini --objective "<objective>"\`\n\nIf no objective was provided, ask the user for one and stop.\n\nAfter the command succeeds, report the new \`TICKET_ID\`.\n"""`
51
+ },
52
+ {
53
+ path: path.join(base, 'prompt.toml'),
54
+ content: `description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nCreate a new Overlord ticket from the user's request.\n\nUse \`{{args}}\` as the input.\nIf it already contains flags such as \`--title\`, \`--priority\`, \`--project-id\`, or \`--execution-target\`, pass those flags through after \`ovld protocol prompt --agent gemini\`.\nOtherwise, treat \`{{args}}\` as the objective text and run:\n\`ovld protocol prompt --agent gemini --objective "<objective>"\`\n\nIf no objective was provided, ask the user for one and stop.\n\nAfter the command succeeds, report the new \`TICKET_ID\` and \`SESSION_KEY\`.\n"""`
55
+ },
56
+ {
57
+ path: path.join(base, 'record-work.toml'),
58
+ content: `description = "Record completed-from-chat work as a ticket in review + feed post (no attach)."\nprompt = """\nImmediately record the work you just completed in this chat as a new Overlord ticket via \`ovld protocol record-work\`. No agent session is opened — the work is already done.\n\nSynthesize from the current conversation:\n- \`objective\`: what was asked / what was done.\n- \`summary\`: reviewer-friendly narrative for the feed.\n- \`changeRationales\`: one entry per meaningful git-tracked file change (\`label\`, \`file_path\`, \`summary\`, \`why\`, \`impact\`, optional \`hunks\`). Use \`git status\` and \`git diff\` to enumerate changed files.\n- \`artifacts\` (optional): \`next_steps\`, \`test_results\`, \`decision\`, \`note\`, \`url\`.\n\nIf \`{{args}}\` is non-empty, treat it as additional context to weave into the summary.\n\nRun \`ovld protocol record-work --payload-file -\` and stream a JSON object \`{ "objective": "...", "summary": "...", "artifacts": [...], "changeRationales": [...] }\` on stdin via a single-quoted heredoc.\n\nAfter the command succeeds, report the new \`TICKET_ID\`.\n\nRules:\n- Do NOT use this for in-progress work. Use \`/prompt\` for that.\n- The CLI validates that every changed git-tracked file is represented in \`changeRationales\` unless \`--skip-file-change-check\` is passed.\n- If project resolution fails, re-run with \`--project-id <id>\` or \`--personal\`.\n"""`
59
+ }
60
+ ];
61
+ }
62
+
63
+ function isRemovableLegacyGeminiCommandFile({ filePath, content, manifestFiles }) {
64
+ if (manifestFiles.has(filePath)) return true;
65
+ const managed = geminiLegacyCommandFiles().find(file => file.path === filePath);
66
+ if (!managed) return false;
67
+ return content.trim() === managed.content.trim();
68
+ }
69
+
70
+ function removeLegacyGeminiConnectorFiles({ readManifest, writeManifest, readTextFile, existsSync = fs.existsSync.bind(fs), rmSync = fs.rmSync.bind(fs) }) {
71
+ const removed = [];
72
+ const manifest = readManifest();
73
+ const manifestFiles = new Set(manifest.gemini?.files ?? []);
74
+
75
+ for (const file of geminiLegacyCommandFiles()) {
76
+ if (!existsSync(file.path)) continue;
77
+ const existing = readTextFile(file.path);
78
+ if (!isRemovableLegacyGeminiCommandFile({ filePath: file.path, content: existing, manifestFiles })) continue;
79
+ rmSync(file.path, { force: true });
80
+ removed.push(file.path);
81
+ }
82
+
83
+ for (const filePath of manifestFiles) {
84
+ if (removed.includes(filePath) || !existsSync(filePath)) continue;
85
+ if (!filePath.includes('.gemini/commands') || !filePath.endsWith('.toml')) continue;
86
+ rmSync(filePath, { force: true });
87
+ removed.push(filePath);
88
+ }
89
+
90
+ if (manifest.gemini) {
91
+ delete manifest.gemini;
92
+ writeManifest(manifest);
93
+ }
94
+
95
+ return removed;
96
+ }
21
97
 
22
98
  const BUNDLE_VERSION = '1.12.0';
23
99
  const MD_MARKER_START = '<!-- overlord:managed:start -->';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605211850.0",
3
+ "version": "0.2605211850.1",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",