@sickr/replay 0.6.0 → 0.7.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/dist/agentAuth.js +76 -0
- package/dist/cli.js +256 -65
- package/dist/share.js +10 -6
- package/package.json +21 -21
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const AGENT_API_URL = (process.env.SICKR_API_URL ?? 'https://support-backend.sickr.ai').replace(/\/+$/, '');
|
|
5
|
+
function agentCredsPath() {
|
|
6
|
+
return join(homedir(), '.sickr', 'agent.json');
|
|
7
|
+
}
|
|
8
|
+
export function readAgentCredentials() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(agentCredsPath(), 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function writeAgentCredentials(c) {
|
|
17
|
+
const dir = join(homedir(), '.sickr');
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
const p = agentCredsPath();
|
|
20
|
+
writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
|
|
21
|
+
try {
|
|
22
|
+
chmodSync(p, 0o600);
|
|
23
|
+
}
|
|
24
|
+
catch { /* Windows: skip */ }
|
|
25
|
+
}
|
|
26
|
+
export function clearAgentCredentials() {
|
|
27
|
+
try {
|
|
28
|
+
unlinkSync(agentCredsPath());
|
|
29
|
+
}
|
|
30
|
+
catch { /* none */ }
|
|
31
|
+
}
|
|
32
|
+
export async function startAgentConnect(apiUrl, agentId) {
|
|
33
|
+
const r = await fetch(`${apiUrl.replace(/\/+$/, '')}/agent-connect/start`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ agent_id: agentId }),
|
|
37
|
+
});
|
|
38
|
+
if (!r.ok)
|
|
39
|
+
throw new Error(`agent-connect/start failed: ${r.status}`);
|
|
40
|
+
return await r.json();
|
|
41
|
+
}
|
|
42
|
+
export async function pollAgentConnect(apiUrl, deviceCode) {
|
|
43
|
+
const r = await fetch(`${apiUrl.replace(/\/+$/, '')}/agent-connect/poll`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
47
|
+
});
|
|
48
|
+
if (!r.ok)
|
|
49
|
+
throw new Error(`agent-connect/poll failed: ${r.status}`);
|
|
50
|
+
return await r.json();
|
|
51
|
+
}
|
|
52
|
+
export async function fetchAgentStatus(c) {
|
|
53
|
+
const r = await fetch(`${c.api_url.replace(/\/+$/, '')}/agent/self/status`, {
|
|
54
|
+
headers: { Authorization: `Bearer ${c.api_key}` },
|
|
55
|
+
});
|
|
56
|
+
if (!r.ok)
|
|
57
|
+
throw new Error(`agent/self/status failed: ${r.status}`);
|
|
58
|
+
return await r.json();
|
|
59
|
+
}
|
|
60
|
+
export async function rotateAgentKey(c) {
|
|
61
|
+
const r = await fetch(`${c.api_url.replace(/\/+$/, '')}/agent/self/rotate`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { Authorization: `Bearer ${c.api_key}` },
|
|
64
|
+
});
|
|
65
|
+
if (!r.ok)
|
|
66
|
+
throw new Error(`agent/self/rotate failed: ${r.status}`);
|
|
67
|
+
return await r.json();
|
|
68
|
+
}
|
|
69
|
+
export async function disconnectAgent(c) {
|
|
70
|
+
const r = await fetch(`${c.api_url.replace(/\/+$/, '')}/agent/self/disconnect`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { Authorization: `Bearer ${c.api_key}` },
|
|
73
|
+
});
|
|
74
|
+
if (!r.ok)
|
|
75
|
+
throw new Error(`agent/self/disconnect failed: ${r.status}`);
|
|
76
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -9,69 +9,77 @@ import { mergeHooks, removeHooks } from './hookConfig.js';
|
|
|
9
9
|
import { renderRunHtml, renderCombinedHtml } from './render.js';
|
|
10
10
|
import { buildSharePayload, buildCombinedPayload, publish, PublishError } from './share.js';
|
|
11
11
|
import { readCredentials, writeCredentials, clearCredentials, startDevice, pollDevice, sleep } from './auth.js';
|
|
12
|
+
import { AGENT_API_URL, clearAgentCredentials, disconnectAgent, fetchAgentStatus, pollAgentConnect, readAgentCredentials, rotateAgentKey, startAgentConnect, writeAgentCredentials, } from './agentAuth.js';
|
|
12
13
|
const REPLAY_ENDPOINT = process.env.SICKR_REPLAY_ENDPOINT ?? 'https://sickr.ai/api/replay';
|
|
13
|
-
const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'help'];
|
|
14
|
+
const COMMANDS = ['init', 'record', 'open', 'list', 'share', 'stop', 'clear', 'login', 'logout', 'whoami', 'agent', 'help'];
|
|
14
15
|
export function parseCommand(argv) {
|
|
15
16
|
const c = argv[0];
|
|
16
17
|
return c && COMMANDS.includes(c) ? c : null;
|
|
17
18
|
}
|
|
18
|
-
export const HELP = `SICKR Replay — audit & replay what your AI coding agent did.
|
|
19
|
-
|
|
20
|
-
Records your Claude Code or Codex session (prompts, edits, commands) to a local,
|
|
21
|
-
redacted timeline you can replay — and optionally share as a public link.
|
|
22
|
-
|
|
23
|
-
Why: a durable record of every agent action — a dashcam for your coding agent.
|
|
24
|
-
If your agent (Claude or Codex) loses context or can't reload a past chat, the
|
|
25
|
-
replay log helps you — and it — recall exactly what was just done.
|
|
26
|
-
|
|
27
|
-
Usage: npx @sickr/replay <command> [options]
|
|
28
|
-
|
|
29
|
-
Commands:
|
|
30
|
-
init <agent> Install recording hooks for an agent (REQUIRED — no default)
|
|
31
|
-
and start capturing to ~/.sickr/runs (secrets redacted):
|
|
32
|
-
claude Claude Code (.claude/settings.json)
|
|
33
|
-
codex Codex (.codex/hooks.json — needs Codex 0.133+)
|
|
34
|
-
all both of the above (feeds the combined view)
|
|
35
|
-
Flag: --no-name (label prompts "Human", not your login name)
|
|
36
|
-
open [run] Render a run to a local HTML timeline and open it. 100% local.
|
|
37
|
-
Defaults to the newest run; pass a run id, or --codex/--claude
|
|
38
|
-
for the newest run of that agent. Combine across agents with a
|
|
39
|
-
window: --today, --since <2h|30m|1d>, or --all (interleaved,
|
|
40
|
-
filterable by agent, sortable by prompt/response time).
|
|
41
|
-
share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
|
|
42
|
-
(shows a preview and asks first). Links expire after 24h.
|
|
43
|
-
--open also open the published link in your browser
|
|
44
|
-
--yes skip the confirmation prompt
|
|
45
|
-
Or publish a COMBINED multi-agent view with a window:
|
|
46
|
-
--today / --since <2h|30m|1d> / --all (+ --claude/--codex).
|
|
47
|
-
list List recorded runs, newest first.
|
|
48
|
-
stop Stop recording — removes SICKR's hooks from this project.
|
|
49
|
-
Your recorded runs are kept; run \`init\` to start again.
|
|
50
|
-
clear Delete all local runs in ~/.sickr/runs (asks first).
|
|
51
|
-
login Sign in with GitHub (optional — unlocks persistent shares and
|
|
52
|
-
Replay Pro cohort eligibility). Zero-account use still works.
|
|
53
|
-
logout Forget the local login. Server-side session stays valid until
|
|
54
|
-
it expires; revoke from your account page if needed.
|
|
55
|
-
whoami Show who you're logged in as.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
19
|
+
export const HELP = `SICKR Replay — audit & replay what your AI coding agent did.
|
|
20
|
+
|
|
21
|
+
Records your Claude Code or Codex session (prompts, edits, commands) to a local,
|
|
22
|
+
redacted timeline you can replay — and optionally share as a public link.
|
|
23
|
+
|
|
24
|
+
Why: a durable record of every agent action — a dashcam for your coding agent.
|
|
25
|
+
If your agent (Claude or Codex) loses context or can't reload a past chat, the
|
|
26
|
+
replay log helps you — and it — recall exactly what was just done.
|
|
27
|
+
|
|
28
|
+
Usage: npx @sickr/replay <command> [options]
|
|
29
|
+
|
|
30
|
+
Commands:
|
|
31
|
+
init <agent> Install recording hooks for an agent (REQUIRED — no default)
|
|
32
|
+
and start capturing to ~/.sickr/runs (secrets redacted):
|
|
33
|
+
claude Claude Code (.claude/settings.json)
|
|
34
|
+
codex Codex (.codex/hooks.json — needs Codex 0.133+)
|
|
35
|
+
all both of the above (feeds the combined view)
|
|
36
|
+
Flag: --no-name (label prompts "Human", not your login name)
|
|
37
|
+
open [run] Render a run to a local HTML timeline and open it. 100% local.
|
|
38
|
+
Defaults to the newest run; pass a run id, or --codex/--claude
|
|
39
|
+
for the newest run of that agent. Combine across agents with a
|
|
40
|
+
window: --today, --since <2h|30m|1d>, or --all (interleaved,
|
|
41
|
+
filterable by agent, sortable by prompt/response time).
|
|
42
|
+
share [run] Redact and publish ONE run to a public sickr.ai/r/<id> link
|
|
43
|
+
(shows a preview and asks first). Links expire after 24h.
|
|
44
|
+
--open also open the published link in your browser
|
|
45
|
+
--yes skip the confirmation prompt
|
|
46
|
+
Or publish a COMBINED multi-agent view with a window:
|
|
47
|
+
--today / --since <2h|30m|1d> / --all (+ --claude/--codex).
|
|
48
|
+
list List recorded runs, newest first.
|
|
49
|
+
stop Stop recording — removes SICKR's hooks from this project.
|
|
50
|
+
Your recorded runs are kept; run \`init\` to start again.
|
|
51
|
+
clear Delete all local runs in ~/.sickr/runs (asks first).
|
|
52
|
+
login Sign in with GitHub (optional — unlocks persistent shares and
|
|
53
|
+
Replay Pro cohort eligibility). Zero-account use still works.
|
|
54
|
+
logout Forget the local login. Server-side session stays valid until
|
|
55
|
+
it expires; revoke from your account page if needed.
|
|
56
|
+
whoami Show who you're logged in as.
|
|
57
|
+
agent connect --agent-id <id>
|
|
58
|
+
Connect this machine to a configured SICKR agent using GitHub
|
|
59
|
+
browser approval. Stores the agent key in ~/.sickr/agent.json.
|
|
60
|
+
agent status Show the connected agent, org and team.
|
|
61
|
+
agent rotate Rotate this machine's agent key.
|
|
62
|
+
agent disconnect
|
|
63
|
+
Revoke this machine's agent key and remove it locally.
|
|
64
|
+
help Show this help.
|
|
65
|
+
|
|
66
|
+
Requires Node 18+. Codex capture needs Codex CLI 0.133+ (run /hooks to trust);
|
|
67
|
+
Claude Code: any hooks-capable build.
|
|
68
|
+
|
|
69
|
+
────────────────────────────────────────────────────────────────────
|
|
70
|
+
This replays your AI coding agents on ONE machine. SICKR governs your whole team.
|
|
71
|
+
Issue tracking + your team + automation + agents — one governed workflow for
|
|
72
|
+
audit, accountability, productivity and confidence.
|
|
73
|
+
|
|
74
|
+
· Gates & approvals — work holds at plan sign-off, review, merge and
|
|
75
|
+
validation checks until each one passes.
|
|
76
|
+
· Humans + agents on one board — agents are first-class teammates with
|
|
77
|
+
roles, capacity and accountability, not a side channel.
|
|
78
|
+
· A full, signed-off audit trail across every actor and every change.
|
|
79
|
+
· Runs 24/7 — produce as much work as you like; the team handles it.
|
|
80
|
+
|
|
81
|
+
Free tier available · bring your own Claude or Codex subscription.
|
|
82
|
+
→ https://sickr.ai
|
|
75
83
|
`;
|
|
76
84
|
export function currentRunId(cc) {
|
|
77
85
|
return String(cc.session_id ?? 'session');
|
|
@@ -350,17 +358,24 @@ async function confirmPublish(yes, what) {
|
|
|
350
358
|
}
|
|
351
359
|
return true;
|
|
352
360
|
}
|
|
353
|
-
/** Publish with a single friendly retry on 429 (the WAF allows ~1/10s).
|
|
361
|
+
/** Publish with a single friendly retry on 429 (the WAF allows ~1/10s).
|
|
362
|
+
* If the user is logged in, attach their token so the share lands in the
|
|
363
|
+
* 7-day daily slot (free_authed) instead of the 24h anon dedup pool. */
|
|
354
364
|
async function publishWithRetry(payload) {
|
|
365
|
+
const token = readCredentials()?.token ?? null;
|
|
366
|
+
const post = async () => {
|
|
367
|
+
const r = await publish(payload, REPLAY_ENDPOINT, { token });
|
|
368
|
+
return { url: r.url, ttl_days: r.ttl_days ?? 1 };
|
|
369
|
+
};
|
|
355
370
|
try {
|
|
356
|
-
return
|
|
371
|
+
return await post();
|
|
357
372
|
}
|
|
358
373
|
catch (err) {
|
|
359
374
|
if (err instanceof PublishError && err.status === 429) {
|
|
360
375
|
process.stdout.write('sickr: rate-limited — you can publish about once every 10s. Waiting to retry once...\n');
|
|
361
376
|
await new Promise((r) => setTimeout(r, 11_000));
|
|
362
377
|
try {
|
|
363
|
-
return
|
|
378
|
+
return await post();
|
|
364
379
|
}
|
|
365
380
|
catch (retryErr) {
|
|
366
381
|
if (retryErr instanceof PublishError && retryErr.status === 429) {
|
|
@@ -373,6 +388,13 @@ async function publishWithRetry(payload) {
|
|
|
373
388
|
throw err;
|
|
374
389
|
}
|
|
375
390
|
}
|
|
391
|
+
function expiryLine(ttl_days) {
|
|
392
|
+
if (ttl_days >= 30)
|
|
393
|
+
return `sickr: this link is live for ${ttl_days} days (Replay Pro retention).\n`;
|
|
394
|
+
if (ttl_days >= 2)
|
|
395
|
+
return `sickr: this link is live for ${ttl_days} days — re-share before it expires to extend.\n`;
|
|
396
|
+
return `sickr: this link expires in 24h. Run \`replay login\` to extend to 7 days.\n`;
|
|
397
|
+
}
|
|
376
398
|
async function handleShare(runId, yes, open) {
|
|
377
399
|
const id = runId ?? latestRunId();
|
|
378
400
|
if (!id) {
|
|
@@ -385,8 +407,8 @@ async function handleShare(runId, yes, open) {
|
|
|
385
407
|
`sickr: tip — run \`npx @sickr/replay open ${id}\` to review the full timeline locally before sharing.\n`);
|
|
386
408
|
if (!(await confirmPublish(yes, 'this run')))
|
|
387
409
|
return;
|
|
388
|
-
const url = await publishWithRetry(payload);
|
|
389
|
-
process.stdout.write(`sickr: published → ${url}\
|
|
410
|
+
const { url, ttl_days } = await publishWithRetry(payload);
|
|
411
|
+
process.stdout.write(`sickr: published → ${url}\n${expiryLine(ttl_days)}sickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n`);
|
|
390
412
|
if (open)
|
|
391
413
|
openInBrowser(url);
|
|
392
414
|
}
|
|
@@ -406,8 +428,8 @@ async function handleShareCombined(sel, yes, open) {
|
|
|
406
428
|
`sickr: tip — run the matching \`open\` window to review locally before sharing.\n`);
|
|
407
429
|
if (!(await confirmPublish(yes, 'this combined replay')))
|
|
408
430
|
return;
|
|
409
|
-
const url = await publishWithRetry(payload);
|
|
410
|
-
process.stdout.write(`sickr: published → ${url}\
|
|
431
|
+
const { url, ttl_days } = await publishWithRetry(payload);
|
|
432
|
+
process.stdout.write(`sickr: published → ${url}\n${expiryLine(ttl_days)}sickr: Replay Pro (live + remote) — early access, rolling out in cohorts → https://sickr.ai/#waitlist\n`);
|
|
411
433
|
if (open)
|
|
412
434
|
openInBrowser(url);
|
|
413
435
|
}
|
|
@@ -498,6 +520,152 @@ export function handleWhoami() {
|
|
|
498
520
|
}
|
|
499
521
|
process.stdout.write(`sickr: ${c.login}${c.name ? ` (${c.name})` : ''} · since ${c.login_at}\n`);
|
|
500
522
|
}
|
|
523
|
+
function valueAt(rest, flag) {
|
|
524
|
+
const i = rest.indexOf(flag);
|
|
525
|
+
return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('-') ? rest[i + 1] : null;
|
|
526
|
+
}
|
|
527
|
+
function isRecord(value) {
|
|
528
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
529
|
+
}
|
|
530
|
+
function agentContextLabel(context) {
|
|
531
|
+
const root = isRecord(context) ? context : {};
|
|
532
|
+
const agent = isRecord(root.agent) ? root.agent : {};
|
|
533
|
+
const org = isRecord(root.org) ? root.org : {};
|
|
534
|
+
const team = isRecord(root.team) ? root.team : {};
|
|
535
|
+
return {
|
|
536
|
+
agentId: typeof agent.agent_id === 'string' ? agent.agent_id : 'unknown',
|
|
537
|
+
provider: typeof agent.provider === 'string' ? agent.provider : 'unknown',
|
|
538
|
+
status: typeof agent.status === 'string' ? agent.status : 'unknown',
|
|
539
|
+
org: typeof org.name === 'string' ? org.name : 'unknown org',
|
|
540
|
+
team: typeof team.name === 'string' ? team.name : 'unknown team',
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
export async function handleAgentStatus() {
|
|
544
|
+
const c = readAgentCredentials();
|
|
545
|
+
if (!c) {
|
|
546
|
+
process.stdout.write('sickr: agent is not connected. Run `sickr agent connect --agent-id <id>`.\n');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const label = agentContextLabel(await fetchAgentStatus(c));
|
|
551
|
+
process.stdout.write(`sickr: connected as ${label.agentId} (${label.provider}, ${label.status})\n` +
|
|
552
|
+
`org: ${label.org} team: ${label.team}\n` +
|
|
553
|
+
`api: ${c.api_url}\n`);
|
|
554
|
+
}
|
|
555
|
+
catch (e) {
|
|
556
|
+
process.stderr.write(`sickr: agent status failed (${e.message}).\n`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
export async function handleAgentRotate() {
|
|
561
|
+
const c = readAgentCredentials();
|
|
562
|
+
if (!c) {
|
|
563
|
+
process.stderr.write('sickr: agent is not connected. Run `sickr agent connect --agent-id <id>`.\n');
|
|
564
|
+
process.exit(1);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const rotated = await rotateAgentKey(c);
|
|
569
|
+
writeAgentCredentials({ ...c, api_key: rotated.api_key, key_id: rotated.key?.id ?? c.key_id });
|
|
570
|
+
process.stdout.write(`sickr: rotated key for ${c.agent_id}.\n`);
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
process.stderr.write(`sickr: agent rotate failed (${e.message}).\n`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
export async function handleAgentDisconnect() {
|
|
578
|
+
const c = readAgentCredentials();
|
|
579
|
+
if (!c) {
|
|
580
|
+
process.stdout.write('sickr: agent is not connected.\n');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
await disconnectAgent(c);
|
|
585
|
+
clearAgentCredentials();
|
|
586
|
+
process.stdout.write(`sickr: disconnected ${c.agent_id} and removed the local key.\n`);
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
process.stderr.write(`sickr: agent disconnect failed (${e.message}).\n`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function storeApprovedAgent(apiUrl, result) {
|
|
594
|
+
writeAgentCredentials({
|
|
595
|
+
api_url: apiUrl,
|
|
596
|
+
agent_id: result.agent_id,
|
|
597
|
+
api_key: result.api_key,
|
|
598
|
+
key_id: result.key_id ?? null,
|
|
599
|
+
org_id: result.org_id,
|
|
600
|
+
team_id: result.team_id,
|
|
601
|
+
connected_at: new Date().toISOString(),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
export async function handleAgentConnect(rest) {
|
|
605
|
+
const agentId = valueAt(rest, '--agent-id') ?? rest.find((a) => !a.startsWith('-')) ?? null;
|
|
606
|
+
if (!agentId) {
|
|
607
|
+
process.stderr.write('sickr: `agent connect` requires --agent-id <id>.\n');
|
|
608
|
+
process.exit(1);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const apiUrl = (valueAt(rest, '--api-url') ?? AGENT_API_URL).replace(/\/+$/, '');
|
|
612
|
+
let started;
|
|
613
|
+
try {
|
|
614
|
+
started = await startAgentConnect(apiUrl, agentId);
|
|
615
|
+
}
|
|
616
|
+
catch (e) {
|
|
617
|
+
process.stderr.write(`sickr: could not start agent connect (${e.message}).\n`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const verifyUrl = started.verification_uri_complete ?? started.verification_uri;
|
|
622
|
+
process.stdout.write(`\nsickr: approve agent ${agentId} in your browser:\n\n` +
|
|
623
|
+
` ${verifyUrl}\n\n` +
|
|
624
|
+
`code: ${started.user_code}\n` +
|
|
625
|
+
`(opening browser...)\n`);
|
|
626
|
+
openInBrowser(verifyUrl);
|
|
627
|
+
const deadline = Date.now() + started.expires_in * 1000;
|
|
628
|
+
const intervalMs = Math.max(1, started.interval) * 1000;
|
|
629
|
+
while (Date.now() < deadline) {
|
|
630
|
+
await sleep(intervalMs);
|
|
631
|
+
let polled;
|
|
632
|
+
try {
|
|
633
|
+
polled = await pollAgentConnect(apiUrl, started.device_code);
|
|
634
|
+
}
|
|
635
|
+
catch (e) {
|
|
636
|
+
process.stderr.write(`sickr: agent connect poll failed (${e.message}).\n`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (polled.status === 'pending')
|
|
641
|
+
continue;
|
|
642
|
+
if (polled.status === 'approved') {
|
|
643
|
+
storeApprovedAgent(apiUrl, polled);
|
|
644
|
+
process.stdout.write(`\nsickr: connected ${polled.agent_id}. Run \`sickr agent status\` to verify.\n`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (polled.status === 'expired') {
|
|
648
|
+
process.stderr.write('sickr: agent connect code expired. Run `sickr agent connect` again.\n');
|
|
649
|
+
process.exit(1);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (polled.status === 'denied') {
|
|
653
|
+
process.stderr.write('sickr: agent connect was denied.\n');
|
|
654
|
+
process.exit(1);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (polled.status === 'consumed') {
|
|
658
|
+
process.stderr.write('sickr: agent connect code was already used. Run `sickr agent connect` again.\n');
|
|
659
|
+
process.exit(1);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
process.stderr.write(`sickr: agent connect error: ${polled.error ?? 'unknown'}.\n`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
process.stderr.write('sickr: agent connect timed out. Run `sickr agent connect` again.\n');
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
501
669
|
async function readStdin() {
|
|
502
670
|
const chunks = [];
|
|
503
671
|
for await (const chunk of process.stdin)
|
|
@@ -563,6 +731,29 @@ async function main() {
|
|
|
563
731
|
case 'whoami':
|
|
564
732
|
handleWhoami();
|
|
565
733
|
return;
|
|
734
|
+
case 'agent': {
|
|
735
|
+
const sub = rest[0];
|
|
736
|
+
const agentRest = rest.slice(1);
|
|
737
|
+
if (sub === 'connect') {
|
|
738
|
+
await handleAgentConnect(agentRest);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (sub === 'status') {
|
|
742
|
+
await handleAgentStatus();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (sub === 'disconnect') {
|
|
746
|
+
await handleAgentDisconnect();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (sub === 'rotate') {
|
|
750
|
+
await handleAgentRotate();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
process.stderr.write('sickr: unknown agent command. Use `sickr agent connect|status|disconnect|rotate`.\n');
|
|
754
|
+
process.exit(1);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
566
757
|
case 'share': {
|
|
567
758
|
const yes = rest.includes('--yes') || rest.includes('-y');
|
|
568
759
|
const openAfter = rest.includes('--open');
|
package/dist/share.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib';
|
|
1
2
|
/** Strip the local id; events are already redacted at capture time. */
|
|
2
3
|
export function buildSharePayload(run) {
|
|
3
4
|
return { run: { cwd: run.cwd, startedAt: run.startedAt, events: run.events } };
|
|
@@ -14,12 +15,15 @@ export class PublishError extends Error {
|
|
|
14
15
|
this.name = 'PublishError';
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
export async function publish(payload, endpoint) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
18
|
+
export async function publish(payload, endpoint, opts = {}) {
|
|
19
|
+
// Gzip the JSON body: ~5-10x smaller for typical sessions, comfortably fits
|
|
20
|
+
// the worker's 4 MB cap even for combined multi-day windows.
|
|
21
|
+
const raw = JSON.stringify(payload);
|
|
22
|
+
const gz = gzipSync(raw);
|
|
23
|
+
const headers = { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' };
|
|
24
|
+
if (opts.token)
|
|
25
|
+
headers.Authorization = `Bearer ${opts.token}`;
|
|
26
|
+
const res = await fetch(endpoint, { method: 'POST', headers, body: gz });
|
|
23
27
|
if (!res.ok)
|
|
24
28
|
throw new PublishError(res.status);
|
|
25
29
|
return (await res.json());
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sickr/replay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "npx @sickr/replay — local Claude Code audit + one-click share. The free wedge into SICKR.",
|
|
6
|
-
"bin": { "replay": "dist/cli.js" },
|
|
7
|
-
"files": ["dist"],
|
|
8
|
-
"publishConfig": { "access": "public" },
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"test": "vitest run",
|
|
12
|
-
"dev": "tsc -w"
|
|
13
|
-
},
|
|
14
|
-
"engines": { "node": ">=20" },
|
|
15
|
-
"license": "UNLICENSED",
|
|
16
|
-
"devDependencies": {
|
|
17
|
-
"@types/node": "^20.14.0",
|
|
18
|
-
"typescript": "^5.6.2",
|
|
19
|
-
"vitest": "^2.1.1"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@sickr/replay",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "npx @sickr/replay — local Claude Code audit + one-click share. The free wedge into SICKR.",
|
|
6
|
+
"bin": { "replay": "dist/cli.js", "sickr": "dist/cli.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"dev": "tsc -w"
|
|
13
|
+
},
|
|
14
|
+
"engines": { "node": ">=20" },
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.14.0",
|
|
18
|
+
"typescript": "^5.6.2",
|
|
19
|
+
"vitest": "^2.1.1"
|
|
20
|
+
}
|
|
21
|
+
}
|