overlord-cli 0.2606081442.0 → 0.2606091312.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/device-identity.d.mts +12 -0
- package/bin/_cli/device-identity.mjs +92 -0
- package/bin/_cli/protocol.mjs +85 -37
- package/bin/_cli/runner.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const CANONICAL_DEVICE_FILE: string;
|
|
2
|
+
|
|
3
|
+
export function getLegacyDesktopDeviceFilePath(userDataPath: string): string;
|
|
4
|
+
|
|
5
|
+
export function readOrCreateCanonicalDeviceFingerprintSync(input?: {
|
|
6
|
+
explicitFingerprint?: string | null | undefined;
|
|
7
|
+
legacyDesktopUserDataPath?: string | null | undefined;
|
|
8
|
+
}): string;
|
|
9
|
+
|
|
10
|
+
export function readOrCreateCanonicalDeviceFingerprint(input?: {
|
|
11
|
+
legacyDesktopUserDataPath?: string | null | undefined;
|
|
12
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const CANONICAL_DEVICE_FILE = path.join(os.homedir(), '.ovld', 'device.json');
|
|
8
|
+
|
|
9
|
+
export function getLegacyDesktopDeviceFilePath(userDataPath) {
|
|
10
|
+
return path.join(userDataPath, 'overlord-device.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseDeviceFingerprint(raw) {
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
return typeof parsed.deviceFingerprint === 'string' ? parsed.deviceFingerprint.trim() : '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function serializeDeviceFingerprint(deviceFingerprint) {
|
|
19
|
+
return `${JSON.stringify({ deviceFingerprint }, null, 2)}\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readDeviceFingerprintSync(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
const fingerprint = parseDeviceFingerprint(fs.readFileSync(filePath, 'utf8'));
|
|
25
|
+
return fingerprint || null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readDeviceFingerprint(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
const fingerprint = parseDeviceFingerprint(await fsp.readFile(filePath, 'utf8'));
|
|
34
|
+
return fingerprint || null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeCanonicalDeviceFingerprintSync(deviceFingerprint) {
|
|
41
|
+
fs.mkdirSync(path.dirname(CANONICAL_DEVICE_FILE), { recursive: true });
|
|
42
|
+
fs.writeFileSync(CANONICAL_DEVICE_FILE, serializeDeviceFingerprint(deviceFingerprint), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function writeCanonicalDeviceFingerprint(deviceFingerprint) {
|
|
46
|
+
await fsp.mkdir(path.dirname(CANONICAL_DEVICE_FILE), { recursive: true });
|
|
47
|
+
await fsp.writeFile(CANONICAL_DEVICE_FILE, serializeDeviceFingerprint(deviceFingerprint), 'utf8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readOrCreateCanonicalDeviceFingerprintSync({
|
|
51
|
+
explicitFingerprint,
|
|
52
|
+
legacyDesktopUserDataPath
|
|
53
|
+
} = {}) {
|
|
54
|
+
const explicit = typeof explicitFingerprint === 'string' ? explicitFingerprint.trim() : '';
|
|
55
|
+
if (explicit) return explicit;
|
|
56
|
+
|
|
57
|
+
const canonical = readDeviceFingerprintSync(CANONICAL_DEVICE_FILE);
|
|
58
|
+
if (canonical) return canonical;
|
|
59
|
+
|
|
60
|
+
if (legacyDesktopUserDataPath) {
|
|
61
|
+
const legacy = readDeviceFingerprintSync(getLegacyDesktopDeviceFilePath(legacyDesktopUserDataPath));
|
|
62
|
+
if (legacy) {
|
|
63
|
+
writeCanonicalDeviceFingerprintSync(legacy);
|
|
64
|
+
return legacy;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const deviceFingerprint = randomUUID();
|
|
69
|
+
writeCanonicalDeviceFingerprintSync(deviceFingerprint);
|
|
70
|
+
return deviceFingerprint;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function readOrCreateCanonicalDeviceFingerprint({
|
|
74
|
+
legacyDesktopUserDataPath
|
|
75
|
+
} = {}) {
|
|
76
|
+
const canonical = await readDeviceFingerprint(CANONICAL_DEVICE_FILE);
|
|
77
|
+
if (canonical) return canonical;
|
|
78
|
+
|
|
79
|
+
if (legacyDesktopUserDataPath) {
|
|
80
|
+
const legacy = await readDeviceFingerprint(
|
|
81
|
+
getLegacyDesktopDeviceFilePath(legacyDesktopUserDataPath)
|
|
82
|
+
);
|
|
83
|
+
if (legacy) {
|
|
84
|
+
await writeCanonicalDeviceFingerprint(legacy);
|
|
85
|
+
return legacy;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const deviceFingerprint = randomUUID();
|
|
90
|
+
await writeCanonicalDeviceFingerprint(deviceFingerprint);
|
|
91
|
+
return deviceFingerprint;
|
|
92
|
+
}
|
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -703,6 +703,19 @@ function resolveGitRepoRoot(workspacePath) {
|
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
// The server caps snapshot.diffStat at 20,000 chars (snapshotContextSchema in
|
|
707
|
+
// lib/overlord/validation.ts). A `git diff --stat` over a large change set can
|
|
708
|
+
// blow past that and make the update POST fail validation. diffstat is purely
|
|
709
|
+
// informational provenance, so truncate it to a safe size before sending.
|
|
710
|
+
const MAX_DIFFSTAT_CHARS = 16_000;
|
|
711
|
+
|
|
712
|
+
function clampDiffStat(diffStat) {
|
|
713
|
+
if (typeof diffStat !== 'string' || diffStat.length <= MAX_DIFFSTAT_CHARS) {
|
|
714
|
+
return diffStat || null;
|
|
715
|
+
}
|
|
716
|
+
return `${diffStat.slice(0, MAX_DIFFSTAT_CHARS)}\n… (diffstat truncated; ${diffStat.length} chars total)`;
|
|
717
|
+
}
|
|
718
|
+
|
|
706
719
|
/**
|
|
707
720
|
* Snapshot the working tree to a hidden ref refs/overlord/checkpoints/<objectiveId>.
|
|
708
721
|
* Captures tracked + untracked + staged + unstaged in one commit-tree object.
|
|
@@ -727,9 +740,11 @@ function createLocalCheckpoint({ flags, kind = 'objective', objectiveId, snapsho
|
|
|
727
740
|
const gitCommitId = runLocalCommand('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], {
|
|
728
741
|
cwd: repoRoot
|
|
729
742
|
}).trim();
|
|
730
|
-
const diffStat =
|
|
731
|
-
|
|
732
|
-
|
|
743
|
+
const diffStat = clampDiffStat(
|
|
744
|
+
runLocalCommand('git', ['-C', repoRoot, 'diff', '--stat', 'HEAD'], {
|
|
745
|
+
cwd: repoRoot
|
|
746
|
+
}).trim()
|
|
747
|
+
);
|
|
733
748
|
return {
|
|
734
749
|
checkpoint: { diffStat: diffStat || null, kind },
|
|
735
750
|
snapshot: {
|
|
@@ -761,9 +776,11 @@ function createLocalCheckpoint({ flags, kind = 'objective', objectiveId, snapsho
|
|
|
761
776
|
if (existingSha) {
|
|
762
777
|
let diffStat = '';
|
|
763
778
|
try {
|
|
764
|
-
diffStat =
|
|
765
|
-
|
|
766
|
-
|
|
779
|
+
diffStat = clampDiffStat(
|
|
780
|
+
runLocalCommand('git', ['-C', repoRoot, 'diff', '--stat', `${existingSha}^!`], {
|
|
781
|
+
cwd: repoRoot
|
|
782
|
+
}).trim()
|
|
783
|
+
);
|
|
767
784
|
} catch {
|
|
768
785
|
/* ignore */
|
|
769
786
|
}
|
|
@@ -812,9 +829,11 @@ function createLocalCheckpoint({ flags, kind = 'objective', objectiveId, snapsho
|
|
|
812
829
|
});
|
|
813
830
|
let diffStat = '';
|
|
814
831
|
try {
|
|
815
|
-
diffStat =
|
|
816
|
-
|
|
817
|
-
|
|
832
|
+
diffStat = clampDiffStat(
|
|
833
|
+
runLocalCommand('git', ['-C', repoRoot, 'diff', '--stat', `${commit}^!`], {
|
|
834
|
+
cwd: repoRoot
|
|
835
|
+
}).trim()
|
|
836
|
+
);
|
|
818
837
|
} catch {
|
|
819
838
|
/* ignore */
|
|
820
839
|
}
|
|
@@ -947,22 +966,31 @@ async function createAndRecordPendingCheckpoints({
|
|
|
947
966
|
continue;
|
|
948
967
|
}
|
|
949
968
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
969
|
+
try {
|
|
970
|
+
await apiPost(
|
|
971
|
+
platformUrl,
|
|
972
|
+
bearerToken,
|
|
973
|
+
localSecret,
|
|
974
|
+
organizationId,
|
|
975
|
+
'/api/protocol/update',
|
|
976
|
+
{
|
|
977
|
+
sessionKey,
|
|
978
|
+
ticketId,
|
|
979
|
+
summary: `Created local git checkpoint for objective ${objectiveId}.`,
|
|
980
|
+
phase: 'execute',
|
|
981
|
+
snapshot: checkpointResult.snapshot
|
|
982
|
+
},
|
|
983
|
+
timeoutMs
|
|
984
|
+
);
|
|
985
|
+
recorded.push(objectiveId);
|
|
986
|
+
} catch (error) {
|
|
987
|
+
// A failed checkpoint update must not block the remaining objectives or
|
|
988
|
+
// the attach itself. The session key is already persisted by the caller.
|
|
989
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
990
|
+
process.stderr.write(
|
|
991
|
+
`[protocol] Failed to record checkpoint for objective ${objectiveId}: ${message}\n`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
966
994
|
}
|
|
967
995
|
return recorded;
|
|
968
996
|
}
|
|
@@ -1275,25 +1303,45 @@ async function protocolAttach(args) {
|
|
|
1275
1303
|
timeoutMs
|
|
1276
1304
|
);
|
|
1277
1305
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1306
|
+
// Persist the new session key BEFORE any best-effort follow-up work. The
|
|
1307
|
+
// checkpoint step below posts a separate /update call whose payload can be
|
|
1308
|
+
// rejected by the server (e.g. a large diffstat exceeding the size cap). If
|
|
1309
|
+
// that throws, we must not lose the freshly minted session key — otherwise a
|
|
1310
|
+
// stale key from a previous ticket lingers in the persisted session file and
|
|
1311
|
+
// every later `ovld protocol` call in this directory resolves the wrong
|
|
1312
|
+
// session. Persisting first guarantees the new key survives a checkpoint
|
|
1313
|
+
// failure. See ticket 1:1430.
|
|
1314
|
+
const sessionKey = data.session?.sessionKey;
|
|
1315
|
+
if (sessionKey) {
|
|
1316
|
+
persistSession(sessionKey, ticketId);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Best-effort: record local git checkpoints for any pending objectives. This
|
|
1320
|
+
// must never abort the attach or prevent the session key from being persisted
|
|
1321
|
+
// and emitted, so swallow any failure (size limits, network, no git repo).
|
|
1322
|
+
let recordedCheckpointObjectiveIds = [];
|
|
1323
|
+
try {
|
|
1324
|
+
recordedCheckpointObjectiveIds = await createAndRecordPendingCheckpoints({
|
|
1325
|
+
attachData: data,
|
|
1326
|
+
bearerToken,
|
|
1327
|
+
flags,
|
|
1328
|
+
localSecret,
|
|
1329
|
+
organizationId,
|
|
1330
|
+
platformUrl,
|
|
1331
|
+
ticketId,
|
|
1332
|
+
timeoutMs
|
|
1333
|
+
});
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1336
|
+
process.stderr.write(`[protocol] Checkpoint recording skipped (attach still succeeded): ${message}\n`);
|
|
1337
|
+
}
|
|
1288
1338
|
if (recordedCheckpointObjectiveIds.length > 0) {
|
|
1289
1339
|
data.recordedCheckpointObjectiveIds = recordedCheckpointObjectiveIds;
|
|
1290
1340
|
}
|
|
1291
1341
|
|
|
1292
|
-
const sessionKey = data.session?.sessionKey;
|
|
1293
1342
|
console.log(JSON.stringify(data, null, 2));
|
|
1294
1343
|
|
|
1295
1344
|
if (sessionKey) {
|
|
1296
|
-
persistSession(sessionKey, ticketId);
|
|
1297
1345
|
// Emit a machine-readable line for easy shell capture:
|
|
1298
1346
|
// SESSION_KEY=$(ovld protocol attach --ticket-id ... | grep ^SESSION_KEY= | cut -d= -f2)
|
|
1299
1347
|
process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
|
package/bin/_cli/runner.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { execFileSync, spawn } from 'node:child_process';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
|
|
6
|
-
import { readOrCreateCanonicalDeviceFingerprintSync } from '
|
|
6
|
+
import { readOrCreateCanonicalDeviceFingerprintSync } from './device-identity.mjs';
|
|
7
7
|
|
|
8
8
|
const OVLD_ENTRY = process.argv[1];
|
|
9
9
|
const DEFAULT_TMUX_COMMAND = 'tmux new-session bash {script}';
|