iteration-cli 0.6.0 → 0.6.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.
- package/dist/cli.js +8 -3
- package/dist/status-view.d.ts +1 -0
- package/dist/status-view.js +5 -0
- package/dist/turn-summary.d.ts +2 -1
- package/dist/turn-summary.js +65 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { activateSource, connectedConfig, detectedSources, expandSource, loadMac
|
|
|
11
11
|
import { importSourceHistory } from "./history-import.js";
|
|
12
12
|
import { runCollector } from "./service.js";
|
|
13
13
|
import { renderDetailedStatus, renderHome, safeWorkspaceName, } from "./status-view.js";
|
|
14
|
-
import { installTurnSummary,
|
|
14
|
+
import { installTurnSummary, codexTurnSummaryApproval, uninstallTurnSummary, } from "./turn-summary.js";
|
|
15
15
|
import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
|
|
16
16
|
import { assertFreshCollectorDirectory, collectorSource, collectorSourceDirectory, collectorPaths, defaultCollectorDirectory, ensureStartableState, healthSnapshot, historyScanProgress, loadConfig, outboxSize, pendingEventIds, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
17
17
|
const DEFAULT_INGEST_ENDPOINT = "https://useiteration.com/api/traces/ingest";
|
|
@@ -135,7 +135,10 @@ function dataDirectory(source, values) {
|
|
|
135
135
|
async function turnSummaryNotice(config) {
|
|
136
136
|
try {
|
|
137
137
|
await installTurnSummary(config);
|
|
138
|
-
|
|
138
|
+
if ((await codexTurnSummaryApproval(config)) === "needs_approval") {
|
|
139
|
+
return "\nOne more step: in Codex, type /hooks and approve Iteration to see your friction summary after each turn.";
|
|
140
|
+
}
|
|
141
|
+
return " A one-line friction summary now appears after agent turns in your repositories.";
|
|
139
142
|
}
|
|
140
143
|
catch (error) {
|
|
141
144
|
return ` The turn summary could not be installed: ${error instanceof Error ? error.message : "unknown error"} Run iteration start ${collectorSource(config)} to retry it.`;
|
|
@@ -360,12 +363,13 @@ async function statusSnapshot(source, values) {
|
|
|
360
363
|
const config = await loadConfig(paths);
|
|
361
364
|
if (collectorSource(config) !== source)
|
|
362
365
|
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
363
|
-
const [running, size, ids, scan, localHealth] = await Promise.all([
|
|
366
|
+
const [running, size, ids, scan, localHealth, approval] = await Promise.all([
|
|
364
367
|
launchAgentStatus(source),
|
|
365
368
|
outboxSize(paths),
|
|
366
369
|
pendingEventIds(paths),
|
|
367
370
|
historyScanProgress(collectorSourceDirectory(config), paths, source),
|
|
368
371
|
healthSnapshot(paths),
|
|
372
|
+
codexTurnSummaryApproval(config),
|
|
369
373
|
]);
|
|
370
374
|
const pendingEvents = size === 0 ? 0 : ids.size;
|
|
371
375
|
const pendingBytes = ids.size === 0 ? 0 : size;
|
|
@@ -378,6 +382,7 @@ async function statusSnapshot(source, values) {
|
|
|
378
382
|
pendingBytes,
|
|
379
383
|
health: localHealth.health,
|
|
380
384
|
healthAvailable: localHealth.kind === "available",
|
|
385
|
+
turnSummaryNeedsApproval: approval === "needs_approval",
|
|
381
386
|
tracesUrl: new URL("/app/sessions", config.endpoint).toString(),
|
|
382
387
|
logPath: path.join(os.homedir(), "Library", "Logs", `iteration-${source}-collector.error.log`),
|
|
383
388
|
};
|
package/dist/status-view.d.ts
CHANGED
package/dist/status-view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectorSourceLabel, } from "iteration-trace-contracts";
|
|
2
|
+
const CODEX_APPROVAL_STEP = "Friction summary is off: in Codex, type /hooks and approve Iteration.";
|
|
2
3
|
function sourceName(source) {
|
|
3
4
|
return collectorSourceLabel(source);
|
|
4
5
|
}
|
|
@@ -197,6 +198,8 @@ export function renderHome(status, nowMs = Date.now(), source = "codex") {
|
|
|
197
198
|
status.health.lastUploadAtMs !== null) {
|
|
198
199
|
lines.push(`Last successful upload ${relativeTime(status.health.lastUploadAtMs, nowMs)}.`);
|
|
199
200
|
}
|
|
201
|
+
if (status.turnSummaryNeedsApproval)
|
|
202
|
+
lines.push("", CODEX_APPROVAL_STEP);
|
|
200
203
|
lines.push("", `Explore traces ${status.tracesUrl}`);
|
|
201
204
|
return lines.join("\n");
|
|
202
205
|
}
|
|
@@ -239,6 +242,8 @@ export function renderDetailedStatus(status, nowMs = Date.now(), source = "codex
|
|
|
239
242
|
: "Local collector error";
|
|
240
243
|
lines.push(`Last issue: ${label} (${relativeTime(failure.atMs, nowMs)})`, `Collector log: ${status.logPath}`);
|
|
241
244
|
}
|
|
245
|
+
if (status.turnSummaryNeedsApproval)
|
|
246
|
+
lines.push(CODEX_APPROVAL_STEP);
|
|
242
247
|
if (!status.running)
|
|
243
248
|
lines.push(`Resume: iteration start ${source}`);
|
|
244
249
|
lines.push(`Explore traces: ${status.tracesUrl}`);
|
package/dist/turn-summary.d.ts
CHANGED
|
@@ -18,4 +18,5 @@ export declare function refreshTurnSummaries(config: CollectorConfig, refreshedA
|
|
|
18
18
|
directory: string;
|
|
19
19
|
atMs: number;
|
|
20
20
|
}>>, nowMs: number, fetcher?: typeof fetch, signal?: AbortSignal): Promise<number>;
|
|
21
|
-
export
|
|
21
|
+
export type TurnSummaryApproval = "approved" | "needs_approval" | "not_installed";
|
|
22
|
+
export declare function codexTurnSummaryApproval(config: CollectorConfig): Promise<TurnSummaryApproval>;
|
package/dist/turn-summary.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { promises as fs } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { isLaunchDirectory, parseCollectorFrictionSummary, } from "iteration-trace-contracts";
|
|
4
5
|
import * as z from "zod";
|
|
5
|
-
import {
|
|
6
|
+
import { ensurePrivateDirectory } from "./storage.js";
|
|
6
7
|
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
7
8
|
const REQUEST_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
8
9
|
const MAX_REFRESHES_PER_PASS = 20;
|
|
@@ -295,8 +296,67 @@ export async function refreshTurnSummaries(config, refreshedAt, nowMs, fetcher =
|
|
|
295
296
|
}
|
|
296
297
|
return refreshed;
|
|
297
298
|
}
|
|
298
|
-
|
|
299
|
-
return
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
function tomlBasicString(value) {
|
|
300
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
301
|
+
}
|
|
302
|
+
// Codex runs a hook only after the developer trusts it in /hooks, and records
|
|
303
|
+
// that trust as a hash of the normalized hook. Recomputing the same hash tells
|
|
304
|
+
// whether Codex will run this exact entry; any other result keeps the reminder.
|
|
305
|
+
function codexHookHash(command) {
|
|
306
|
+
const identity = {
|
|
307
|
+
event_name: "stop",
|
|
308
|
+
hooks: [
|
|
309
|
+
{
|
|
310
|
+
async: false,
|
|
311
|
+
command,
|
|
312
|
+
timeout: HOOK_TIMEOUT_SECONDS,
|
|
313
|
+
type: "command",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
};
|
|
317
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(identity)).digest("hex")}`;
|
|
318
|
+
}
|
|
319
|
+
function codexTrustedHash(configToml, key) {
|
|
320
|
+
const lines = configToml.split(/\r?\n/);
|
|
321
|
+
const header = `[hooks.state.${tomlBasicString(key)}]`;
|
|
322
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
323
|
+
if (start < 0)
|
|
324
|
+
return null;
|
|
325
|
+
let trustedHash = null;
|
|
326
|
+
for (const line of lines.slice(start + 1)) {
|
|
327
|
+
const trimmed = line.trim();
|
|
328
|
+
if (trimmed.startsWith("["))
|
|
329
|
+
break;
|
|
330
|
+
if (/^enabled\s*=\s*false\b/.test(trimmed))
|
|
331
|
+
return null;
|
|
332
|
+
const match = /^trusted_hash\s*=\s*"(sha256:[0-9a-f]{64})"/.exec(trimmed);
|
|
333
|
+
if (match?.[1] !== undefined)
|
|
334
|
+
trustedHash = match[1];
|
|
335
|
+
}
|
|
336
|
+
return trustedHash;
|
|
337
|
+
}
|
|
338
|
+
export async function codexTurnSummaryApproval(config) {
|
|
339
|
+
if ("claudeHome" in config)
|
|
340
|
+
return "approved";
|
|
341
|
+
const filename = agentHookFile(config);
|
|
342
|
+
const { file } = await readHookFile(filename);
|
|
343
|
+
const command = hookCommand(turnSummaryPaths(config.dataDirectory));
|
|
344
|
+
const stop = file.hooks?.Stop ?? [];
|
|
345
|
+
for (const [groupIndex, group] of stop.entries()) {
|
|
346
|
+
const handlerIndex = group.hooks?.findIndex((hook) => hook.command === command) ?? -1;
|
|
347
|
+
if (handlerIndex < 0)
|
|
348
|
+
continue;
|
|
349
|
+
let configToml;
|
|
350
|
+
try {
|
|
351
|
+
configToml = await fs.readFile(path.join(config.codexHome, "config.toml"), "utf8");
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
355
|
+
return "needs_approval";
|
|
356
|
+
throw new Error("Could not read the Codex config.", { cause: error });
|
|
357
|
+
}
|
|
358
|
+
const trusted = codexTrustedHash(configToml, `${filename}:stop:${groupIndex}:${handlerIndex}`);
|
|
359
|
+
return trusted === codexHookHash(command) ? "approved" : "needs_approval";
|
|
360
|
+
}
|
|
361
|
+
return "not_installed";
|
|
302
362
|
}
|