@constraint/cli 0.4.0 → 0.4.2
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/package.json +1 -1
- package/skills/constraint-skills/SKILL.md +4 -4
- package/skills/constraint-skills/references/commands.md +4 -3
- package/src/cli.js +57 -32
- package/src/files.js +3 -1
- package/src/paths.js +4 -7
- package/src/setup.js +7 -73
- package/src/skills.js +36 -0
package/package.json
CHANGED
|
@@ -57,10 +57,10 @@ constraint report <transcript-path> --skill <skill-name>
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Pass the explicit local path to the transcript as it exists at that moment.
|
|
60
|
-
Use the invoked skill's frontmatter `name`.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
Use the invoked skill's frontmatter `name`. Client and session id are read
|
|
61
|
+
from the transcript path; re-reporting the same session later supersedes the
|
|
62
|
+
earlier report. Never create a replacement transcript, summarize it, copy it
|
|
63
|
+
elsewhere first, or pass a temporary file. If the transcript path cannot be
|
|
64
64
|
determined, say so instead of guessing.
|
|
65
65
|
|
|
66
66
|
## Work safely
|
|
@@ -12,7 +12,7 @@ constraint setup
|
|
|
12
12
|
constraint doctor
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code
|
|
15
|
+
`login` uses WorkOS device authorization. Never request or embed an API key or client secret. `setup` detects Codex and Claude Code and installs this skill globally. It never touches instruction files.
|
|
16
16
|
|
|
17
17
|
## Discover, inspect, install, and update
|
|
18
18
|
|
|
@@ -53,8 +53,9 @@ constraint skills run get <run-id> --raw
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
`report` is user-driven: run it only when the user asks to put a session on the
|
|
56
|
-
record.
|
|
57
|
-
|
|
56
|
+
record. Client and session id are read from the transcript path itself (Claude
|
|
57
|
+
Code project transcripts and Codex rollouts are named by session id);
|
|
58
|
+
`--client`/`--session` are only needed for files outside those locations.
|
|
58
59
|
Re-reporting the same session supersedes the earlier report.
|
|
59
60
|
|
|
60
61
|
Repeat `--team`, `--user`, or `--skill` to request subsets. Review visibility is permission-controlled.
|
package/src/cli.js
CHANGED
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
taskSpinner,
|
|
21
21
|
} from './prompts.js';
|
|
22
22
|
import {
|
|
23
|
-
applyInstructionSetup,
|
|
24
23
|
installCompanionSkill,
|
|
25
24
|
setupPlan,
|
|
26
25
|
setupStatus,
|
|
27
26
|
} from './setup.js';
|
|
28
27
|
import {
|
|
29
28
|
configuredAgents,
|
|
29
|
+
identifyLocalSkill,
|
|
30
30
|
installSkill,
|
|
31
31
|
installedSkills,
|
|
32
32
|
manageableDomainRecords,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
uploadSkill,
|
|
37
37
|
} from './skills.js';
|
|
38
38
|
|
|
39
|
-
const VERSION = '0.4.
|
|
39
|
+
const VERSION = '0.4.2';
|
|
40
40
|
|
|
41
41
|
const HELP = `Constraint Skills CLI
|
|
42
42
|
|
|
@@ -249,30 +249,17 @@ async function setupCommand(args, out) {
|
|
|
249
249
|
if (dryRun) {
|
|
250
250
|
out.data(plans.map((plan) => ({
|
|
251
251
|
agent: plan.agent,
|
|
252
|
-
instructions: {
|
|
253
|
-
path: plan.instructions.path,
|
|
254
|
-
action: plan.instructions.changed ? 'update' : 'current',
|
|
255
|
-
content: plan.instructions.content,
|
|
256
|
-
},
|
|
257
252
|
skill: { path: plan.skill.path, scope: 'global', action: plan.skill.action },
|
|
258
253
|
})));
|
|
259
254
|
return;
|
|
260
255
|
}
|
|
261
|
-
const hasInstructionChanges = plans.some((plan) => plan.instructions.changed);
|
|
262
256
|
const hasSkillChanges = plans.some((plan) => plan.skill.action !== 'current');
|
|
263
|
-
if (!useUi && !yes &&
|
|
257
|
+
if (!useUi && !yes && hasSkillChanges) {
|
|
264
258
|
throw new Error('Non-interactive setup has pending changes. Pass --yes or use --dry-run.');
|
|
265
259
|
}
|
|
266
260
|
|
|
267
|
-
let configureInstructions = true;
|
|
268
261
|
let installSkill = true;
|
|
269
262
|
if (useUi) {
|
|
270
|
-
note(
|
|
271
|
-
plans.map((plan) => `${humanize(plan.agent)}\n${plan.instructions.path}\n${plan.instructions.changed ? 'Legacy Constraint block will be removed' : 'Nothing to clean up'}`).join('\n\n'),
|
|
272
|
-
'1. Remove legacy instruction blocks',
|
|
273
|
-
);
|
|
274
|
-
configureInstructions = !hasInstructionChanges || yes || await confirmPlan('Remove the legacy Constraint block from these files?');
|
|
275
|
-
|
|
276
263
|
note(
|
|
277
264
|
[
|
|
278
265
|
'Skill: constraint-skills',
|
|
@@ -284,20 +271,12 @@ async function setupCommand(args, out) {
|
|
|
284
271
|
'',
|
|
285
272
|
]),
|
|
286
273
|
].join('\n').trim(),
|
|
287
|
-
'
|
|
274
|
+
'Install Constraint Skills',
|
|
288
275
|
);
|
|
289
276
|
installSkill = !hasSkillChanges || yes || await confirmPlan(`Install globally for ${agents.map(humanize).join(' and ')}?`);
|
|
290
277
|
}
|
|
291
278
|
|
|
292
|
-
const result = {
|
|
293
|
-
if (configureInstructions) {
|
|
294
|
-
const spinner = taskSpinner(useUi && hasInstructionChanges);
|
|
295
|
-
for (const plan of plans) {
|
|
296
|
-
spinner.message(`Cleaning ${humanize(plan.agent)} instructions`);
|
|
297
|
-
result.instructions.push(await applyInstructionSetup(plan.agent));
|
|
298
|
-
}
|
|
299
|
-
spinner.stop('Agent instructions cleaned.');
|
|
300
|
-
}
|
|
279
|
+
const result = { skill: [] };
|
|
301
280
|
if (installSkill) {
|
|
302
281
|
const spinner = taskSpinner(useUi && hasSkillChanges);
|
|
303
282
|
for (const plan of plans) {
|
|
@@ -309,7 +288,6 @@ async function setupCommand(args, out) {
|
|
|
309
288
|
if (useUi) {
|
|
310
289
|
finish([
|
|
311
290
|
'Setup complete.',
|
|
312
|
-
`Instructions: ${configureInstructions ? `${agents.length} configured` : 'skipped'}`,
|
|
313
291
|
`Skill: ${installSkill ? `installed for ${agents.map(humanize).join(' and ')}` : 'skipped'}`,
|
|
314
292
|
].join('\n'));
|
|
315
293
|
} else out.data(result);
|
|
@@ -472,8 +450,23 @@ async function uploadCommand(args, config, out) {
|
|
|
472
450
|
|
|
473
451
|
// A run is the posting of a transcript in which a skill was invoked. The user
|
|
474
452
|
// drives reporting: one command, after the session, with the finished
|
|
475
|
-
// transcript. Client and session id
|
|
476
|
-
//
|
|
453
|
+
// transcript. Client and session id come straight from the transcript path —
|
|
454
|
+
// agent clients keep sessions at well-known locations named by session id.
|
|
455
|
+
export function transcriptIdentity(absolute) {
|
|
456
|
+
const basename = path.basename(absolute);
|
|
457
|
+
const stem = basename.replace(/\.(jsonl?|json)$/i, '');
|
|
458
|
+
const uuid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
459
|
+
if (absolute.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`) || stem.startsWith('rollout-')) {
|
|
460
|
+
const match = stem.match(uuid);
|
|
461
|
+
return { client: 'codex', session: match ? match[0] : stem };
|
|
462
|
+
}
|
|
463
|
+
if (absolute.includes(`${path.sep}.claude${path.sep}projects${path.sep}`) || uuid.test(stem)) {
|
|
464
|
+
const match = stem.match(uuid);
|
|
465
|
+
return { client: 'claude-code', session: match ? match[0] : stem };
|
|
466
|
+
}
|
|
467
|
+
return { client: null, session: null };
|
|
468
|
+
}
|
|
469
|
+
|
|
477
470
|
async function reportCommand(args, config, out) {
|
|
478
471
|
const skill = takeOption(args, '--skill');
|
|
479
472
|
const client = takeOption(args, '--client');
|
|
@@ -483,9 +476,37 @@ async function reportCommand(args, config, out) {
|
|
|
483
476
|
throw new Error('Usage: constraint report <transcript-path> --skill <skill> [--client <client>] [--session <session-id>]');
|
|
484
477
|
}
|
|
485
478
|
const transcript = await readTranscript(args[0]);
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
479
|
+
const identity = transcriptIdentity(transcript.absolute);
|
|
480
|
+
const resolvedClient = client || identity.client;
|
|
481
|
+
const resolvedSession = session || identity.session;
|
|
482
|
+
if (!resolvedClient || !resolvedSession) {
|
|
483
|
+
throw new Error('Could not tell the client and session from this path. Pass --client and --session.');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Version attribution: hash the local skill folder and match it against the
|
|
487
|
+
// published versions, so the run records what actually ran — not "latest".
|
|
488
|
+
let detail;
|
|
489
|
+
try {
|
|
490
|
+
detail = await apiRequest(config, `/skills/${encodeURIComponent(skill)}`);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (/not found/i.test(error.message)) {
|
|
493
|
+
const projectRoot = await findProjectRoot();
|
|
494
|
+
const hint = await identifyLocalSkill(config, { skill_id: skill, slug: skill, latest_version: 0 }, { projectRoot })
|
|
495
|
+
.then((found) => found.localPath)
|
|
496
|
+
.catch(() => null);
|
|
497
|
+
throw new Error(
|
|
498
|
+
`${skill} is not on Constraint.` +
|
|
499
|
+
(hint ? ` Found a local skill at ${hint} — upload it first:
|
|
500
|
+
constraint skills upload ${hint} --domain <domain>` : ' Upload it first with constraint skills upload.'),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
const projectRoot = await findProjectRoot();
|
|
506
|
+
const identified = await identifyLocalSkill(config, detail, { projectRoot });
|
|
507
|
+
|
|
508
|
+
const query = new URLSearchParams({ skill, client: resolvedClient, session: resolvedSession });
|
|
509
|
+
if (identified.verified) query.set('version', String(identified.version));
|
|
489
510
|
const run = await apiRequest(config, `/skill-reports?${query.toString()}`, {
|
|
490
511
|
method: 'POST',
|
|
491
512
|
headers: { 'Content-Type': mediaType(transcript.absolute), 'X-Transcript-Filename': transcript.filename },
|
|
@@ -495,6 +516,10 @@ async function reportCommand(args, config, out) {
|
|
|
495
516
|
run_id: run.run_id,
|
|
496
517
|
skill: run.skill_slug,
|
|
497
518
|
version: run.skill_version,
|
|
519
|
+
version_verified: identified.verified,
|
|
520
|
+
...(identified.localPath && !identified.verified
|
|
521
|
+
? { warning: `local copy at ${identified.localPath} does not match any published version` }
|
|
522
|
+
: {}),
|
|
498
523
|
state: run.state,
|
|
499
524
|
client: run.client,
|
|
500
525
|
session: run.client_session_id,
|
package/src/files.js
CHANGED
|
@@ -43,7 +43,9 @@ export async function readSkillDirectory(input) {
|
|
|
43
43
|
|
|
44
44
|
export function packageDigest(files) {
|
|
45
45
|
const hash = crypto.createHash('sha256');
|
|
46
|
-
|
|
46
|
+
// Sort by code points, not locale: this digest must match the server's,
|
|
47
|
+
// which sorts paths with plain string comparison.
|
|
48
|
+
for (const file of [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
|
|
47
49
|
hash.update(file.path);
|
|
48
50
|
hash.update('\0');
|
|
49
51
|
hash.update(file.content);
|
package/src/paths.js
CHANGED
|
@@ -28,18 +28,15 @@ export function agentPaths(agent, { scope = 'global', projectRoot = process.cwd(
|
|
|
28
28
|
const selected = normalizeAgent(agent);
|
|
29
29
|
if (scope === 'project') {
|
|
30
30
|
if (selected === 'codex') {
|
|
31
|
-
return { skills: path.join(projectRoot, '.agents', 'skills')
|
|
31
|
+
return { skills: path.join(projectRoot, '.agents', 'skills') };
|
|
32
32
|
}
|
|
33
|
-
return { skills: path.join(projectRoot, '.claude', 'skills')
|
|
33
|
+
return { skills: path.join(projectRoot, '.claude', 'skills') };
|
|
34
34
|
}
|
|
35
35
|
if (selected === 'codex') {
|
|
36
36
|
const root = process.env.CODEX_HOME ? expandPath(process.env.CODEX_HOME) : path.join(os.homedir(), '.codex');
|
|
37
|
-
return {
|
|
37
|
+
return { skills: path.join(root, 'skills') };
|
|
38
38
|
}
|
|
39
|
-
return {
|
|
40
|
-
instructions: path.join(os.homedir(), '.claude', 'CLAUDE.md'),
|
|
41
|
-
skills: path.join(os.homedir(), '.claude', 'skills'),
|
|
42
|
-
};
|
|
39
|
+
return { skills: path.join(os.homedir(), '.claude', 'skills') };
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
async function exists(candidate) {
|
package/src/setup.js
CHANGED
|
@@ -6,32 +6,6 @@ import { loadConfig, saveConfig } from './config.js';
|
|
|
6
6
|
import { packageDigest, readSkillDirectory, writeSkillDirectory } from './files.js';
|
|
7
7
|
import { agentPaths, normalizeAgent } from './paths.js';
|
|
8
8
|
|
|
9
|
-
export const BLOCK_START = '<!-- constraint-skills:start -->';
|
|
10
|
-
export const BLOCK_END = '<!-- constraint-skills:end -->';
|
|
11
|
-
|
|
12
|
-
// Earlier releases injected a managed instruction block into the user's global
|
|
13
|
-
// CLAUDE.md/AGENTS.md. Writing into user-owned instruction files was never
|
|
14
|
-
// trustworthy behavior; setup now only removes that legacy block if present.
|
|
15
|
-
export function stripInstructionBlock(existing) {
|
|
16
|
-
const start = existing.indexOf(BLOCK_START);
|
|
17
|
-
const end = existing.indexOf(BLOCK_END);
|
|
18
|
-
if (start < 0 && end < 0) return existing;
|
|
19
|
-
if ((start >= 0) !== (end >= 0)) {
|
|
20
|
-
throw new Error('The existing Constraint Skills instruction block is incomplete; fix it before running setup.');
|
|
21
|
-
}
|
|
22
|
-
const before = existing.slice(0, start).replace(/\n+$/, '\n');
|
|
23
|
-
const after = existing.slice(end + BLOCK_END.length).replace(/^\n+/, '\n');
|
|
24
|
-
const stripped = `${before}${after}`;
|
|
25
|
-
return stripped.trim() === '' ? '' : stripped.replace(/\n{3,}/g, '\n\n');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function writeInstructions(file, content, mode) {
|
|
29
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
30
|
-
const staging = `${file}.constraint-${process.pid}`;
|
|
31
|
-
await fs.writeFile(staging, content, { mode });
|
|
32
|
-
await fs.rename(staging, file);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
9
|
async function companionFiles() {
|
|
36
10
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
37
11
|
return readSkillDirectory(path.resolve(here, '..', 'skills', 'constraint-skills'));
|
|
@@ -44,19 +18,6 @@ export function expectedCompanionHash(config, selected, currentHash) {
|
|
|
44
18
|
return currentHash && recorded.includes(currentHash) ? currentHash : null;
|
|
45
19
|
}
|
|
46
20
|
|
|
47
|
-
async function instructionPlan(selected, paths) {
|
|
48
|
-
let existing = '';
|
|
49
|
-
let mode = 0o644;
|
|
50
|
-
try {
|
|
51
|
-
existing = await fs.readFile(paths.instructions, 'utf8');
|
|
52
|
-
mode = (await fs.stat(paths.instructions)).mode & 0o777;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (error && error.code !== 'ENOENT') throw error;
|
|
55
|
-
}
|
|
56
|
-
const content = stripInstructionBlock(existing);
|
|
57
|
-
return { path: paths.instructions, changed: content !== existing, content, mode };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
21
|
async function skillPlan(selected, paths) {
|
|
61
22
|
const files = await companionFiles();
|
|
62
23
|
const bundledHash = packageDigest(files);
|
|
@@ -84,7 +45,6 @@ export async function setupPlan(agent) {
|
|
|
84
45
|
const paths = agentPaths(selected);
|
|
85
46
|
return {
|
|
86
47
|
agent: selected,
|
|
87
|
-
instructions: await instructionPlan(selected, paths),
|
|
88
48
|
skill: await skillPlan(selected, paths),
|
|
89
49
|
};
|
|
90
50
|
}
|
|
@@ -95,15 +55,6 @@ async function recordConfiguredAgent(selected, config = null) {
|
|
|
95
55
|
await saveConfig(next);
|
|
96
56
|
}
|
|
97
57
|
|
|
98
|
-
export async function applyInstructionSetup(agent) {
|
|
99
|
-
const selected = normalizeAgent(agent);
|
|
100
|
-
const paths = agentPaths(selected);
|
|
101
|
-
const plan = await instructionPlan(selected, paths);
|
|
102
|
-
if (plan.changed) await writeInstructions(plan.path, plan.content, plan.mode);
|
|
103
|
-
await recordConfiguredAgent(selected);
|
|
104
|
-
return { agent: selected, path: plan.path, changed: plan.changed, applied: true };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
58
|
export async function installCompanionSkill(agent) {
|
|
108
59
|
const selected = normalizeAgent(agent);
|
|
109
60
|
const paths = agentPaths(selected);
|
|
@@ -122,42 +73,25 @@ export async function installCompanionSkill(agent) {
|
|
|
122
73
|
|
|
123
74
|
export async function setupAgent(agent, {
|
|
124
75
|
dryRun = false,
|
|
125
|
-
yes = false,
|
|
126
|
-
confirmChange = async () => false,
|
|
127
76
|
output = console.log,
|
|
128
|
-
instructions = true,
|
|
129
|
-
skill = true,
|
|
130
77
|
} = {}) {
|
|
131
78
|
const selected = normalizeAgent(agent);
|
|
132
79
|
const plan = await setupPlan(selected);
|
|
133
|
-
output(`${selected}: ${plan.instructions.path}`);
|
|
134
80
|
if (dryRun) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return { agent: selected, instructions: plan.instructions, skill: plan.skill, applied: false };
|
|
138
|
-
}
|
|
139
|
-
let instructionResult = null;
|
|
140
|
-
let skillResult = null;
|
|
141
|
-
if (instructions) {
|
|
142
|
-
const approved = !plan.instructions.changed || yes || await confirmChange(
|
|
143
|
-
`Remove the legacy Constraint block from ${plan.instructions.path}?`,
|
|
144
|
-
);
|
|
145
|
-
if (approved) instructionResult = await applyInstructionSetup(selected);
|
|
81
|
+
output(`Install constraint-skills globally at ${plan.skill.path} (${plan.skill.action}).`);
|
|
82
|
+
return { agent: selected, skill: plan.skill, applied: false };
|
|
146
83
|
}
|
|
147
|
-
|
|
148
|
-
return { agent: selected,
|
|
84
|
+
const skillResult = await installCompanionSkill(selected);
|
|
85
|
+
return { agent: selected, skill: skillResult, applied: true };
|
|
149
86
|
}
|
|
150
87
|
|
|
151
88
|
export async function setupStatus(agent) {
|
|
152
89
|
const selected = normalizeAgent(agent);
|
|
153
90
|
const paths = agentPaths(selected);
|
|
154
91
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md')),
|
|
158
|
-
]);
|
|
159
|
-
return { agent: selected, legacy_instruction_block: instructions.includes(BLOCK_START), skill: skill.isFile() };
|
|
92
|
+
const skill = await fs.stat(path.join(paths.skills, 'constraint-skills', 'SKILL.md'));
|
|
93
|
+
return { agent: selected, skill: skill.isFile() };
|
|
160
94
|
} catch {
|
|
161
|
-
return { agent: selected,
|
|
95
|
+
return { agent: selected, skill: false };
|
|
162
96
|
}
|
|
163
97
|
}
|
package/src/skills.js
CHANGED
|
@@ -231,6 +231,42 @@ export async function installedSkills({ scope = 'project', projectRoot, agents }
|
|
|
231
231
|
return Object.values(lock.installations || {}).filter((item) => !selected || selected.has(item.agent));
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Version attribution for reports: find the skill's local folder (managed
|
|
235
|
+
// symlink or plain folder — agents read both from the same directories), hash
|
|
236
|
+
// it with the registry's package digest, and match against published versions.
|
|
237
|
+
export async function identifyLocalSkill(config, detail, { projectRoot = null } = {}) {
|
|
238
|
+
const candidates = [];
|
|
239
|
+
if (projectRoot) {
|
|
240
|
+
for (const agent of ['claude-code', 'codex']) {
|
|
241
|
+
candidates.push(path.join(agentPaths(agent, { scope: 'project', projectRoot }).skills, detail.slug));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const agent of ['claude-code', 'codex']) {
|
|
245
|
+
candidates.push(path.join(agentPaths(agent).skills, detail.slug));
|
|
246
|
+
}
|
|
247
|
+
let local = null;
|
|
248
|
+
for (const candidate of candidates) {
|
|
249
|
+
try {
|
|
250
|
+
const digest = packageDigest(await readSkillDirectory(candidate));
|
|
251
|
+
local = { path: candidate, digest };
|
|
252
|
+
break;
|
|
253
|
+
} catch {
|
|
254
|
+
// not present here; keep looking
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!local) return { localPath: null, version: null, verified: false };
|
|
258
|
+
for (let version = detail.latest_version; version >= 1; version -= 1) {
|
|
259
|
+
const candidate = await apiRequest(
|
|
260
|
+
config,
|
|
261
|
+
`/skills/${encodeURIComponent(detail.skill_id)}/versions/${version}`,
|
|
262
|
+
);
|
|
263
|
+
if (candidate.version.content_sha256 === local.digest) {
|
|
264
|
+
return { localPath: local.path, version, verified: true };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { localPath: local.path, version: null, verified: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
234
270
|
export async function readTranscript(file) {
|
|
235
271
|
const absolute = path.resolve(file);
|
|
236
272
|
return { absolute, filename: path.basename(absolute), content: await fs.readFile(absolute) };
|