teleportation-cli 1.2.2 → 1.4.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/.claude/hooks/permission_request.mjs +11 -4
- package/.claude/hooks/post_tool_use.mjs +1 -3
- package/.claude/hooks/pre_tool_use.mjs +216 -287
- package/.claude/hooks/session-register.mjs +36 -28
- package/.claude/hooks/session_end.mjs +1 -3
- package/.claude/hooks/session_start.mjs +15 -1
- package/.claude/hooks/stop.mjs +215 -224
- package/.claude/hooks/user_prompt_submit.mjs +1 -3
- package/lib/daemon/task-executor-v2.js +208 -27
- package/lib/daemon/teleportation-daemon.js +215 -19
- package/lib/daemon/timeline-analyzer.js +19 -13
- package/lib/daemon/transcript-ingestion.js +152 -44
- package/lib/install/installer.js +43 -13
- package/package.json +1 -1
- package/teleportation-cli.cjs +57 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module lib/daemon/transcript-ingestion
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFile, readdir } from 'fs/promises';
|
|
10
|
+
import { readFile, readdir, writeFile, mkdir } from 'fs/promises';
|
|
11
11
|
import { homedir, tmpdir } from 'os';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
|
|
@@ -132,6 +132,49 @@ const RETRY_BASE_DELAY_MS = 100;
|
|
|
132
132
|
*/
|
|
133
133
|
const DEBUG = process.env.TELEPORTATION_DEBUG === 'true';
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Local cursor directory for tracking last-processed transcript index.
|
|
137
|
+
* Persists across daemon restarts so we don't reprocess the entire transcript.
|
|
138
|
+
*/
|
|
139
|
+
const CURSOR_DIR = join(tmpdir(), 'teleportation-cursors');
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read the local ingestion cursor for a session.
|
|
143
|
+
* Returns the last successfully processed transcript message count.
|
|
144
|
+
* @param {string} sessionId - Session ID
|
|
145
|
+
* @returns {Promise<number>} Last processed message count (0 if no cursor)
|
|
146
|
+
*/
|
|
147
|
+
async function readCursor(sessionId) {
|
|
148
|
+
try {
|
|
149
|
+
const cursorPath = join(CURSOR_DIR, `${sessionId}.json`);
|
|
150
|
+
const data = JSON.parse(await readFile(cursorPath, 'utf8'));
|
|
151
|
+
return data.lastMessageCount || 0;
|
|
152
|
+
} catch {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Write the local ingestion cursor after successful push.
|
|
159
|
+
* @param {string} sessionId - Session ID
|
|
160
|
+
* @param {number} messageCount - Total transcript messages processed
|
|
161
|
+
* @param {number} eventsPushed - Events successfully pushed this cycle
|
|
162
|
+
*/
|
|
163
|
+
async function writeCursor(sessionId, messageCount, eventsPushed) {
|
|
164
|
+
try {
|
|
165
|
+
await mkdir(CURSOR_DIR, { recursive: true });
|
|
166
|
+
const cursorPath = join(CURSOR_DIR, `${sessionId}.json`);
|
|
167
|
+
await writeFile(cursorPath, JSON.stringify({
|
|
168
|
+
lastMessageCount: messageCount,
|
|
169
|
+
lastEventsPushed: eventsPushed,
|
|
170
|
+
updatedAt: new Date().toISOString()
|
|
171
|
+
}));
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// Non-fatal — next cycle will just reprocess some events
|
|
174
|
+
console.warn(`[transcript] Failed to write cursor for ${sessionId}: ${e.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
135
178
|
/**
|
|
136
179
|
* Sleep helper for retry backoff
|
|
137
180
|
* @param {number} ms - Milliseconds to sleep
|
|
@@ -328,7 +371,31 @@ function parseTimestamp(entry) {
|
|
|
328
371
|
return Date.now();
|
|
329
372
|
}
|
|
330
373
|
|
|
331
|
-
const
|
|
374
|
+
const ts = entry.timestamp;
|
|
375
|
+
|
|
376
|
+
// Handle numeric string timestamps (epoch ms as string, e.g. "1771211826406")
|
|
377
|
+
// new Date("1771211826406") returns Invalid Date, so detect and convert first
|
|
378
|
+
if (typeof ts === 'string' && /^\d+$/.test(ts)) {
|
|
379
|
+
const numeric = Number(ts);
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
if (numeric > now - TIMESTAMP_MAX_AGE_MS && numeric < now + TIMESTAMP_MAX_AGE_MS) {
|
|
382
|
+
return numeric;
|
|
383
|
+
}
|
|
384
|
+
console.warn(`[transcript] Numeric string timestamp out of range: ${ts}, using current time`);
|
|
385
|
+
return Date.now();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Handle numeric timestamps directly
|
|
389
|
+
if (typeof ts === 'number') {
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
if (ts > now - TIMESTAMP_MAX_AGE_MS && ts < now + TIMESTAMP_MAX_AGE_MS) {
|
|
392
|
+
return ts;
|
|
393
|
+
}
|
|
394
|
+
console.warn(`[transcript] Numeric timestamp out of range: ${ts}, using current time`);
|
|
395
|
+
return Date.now();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const parsed = new Date(ts).getTime();
|
|
332
399
|
|
|
333
400
|
// Validate timestamp is reasonable (not NaN, not in distant past/future)
|
|
334
401
|
if (isNaN(parsed)) {
|
|
@@ -396,7 +463,7 @@ function extractTimelineEvents(transcript, fromIndex = 0) {
|
|
|
396
463
|
if (trimmedContent) {
|
|
397
464
|
events.push({
|
|
398
465
|
type: 'assistant_response',
|
|
399
|
-
source:
|
|
466
|
+
source: 'cli_interactive',
|
|
400
467
|
timestamp,
|
|
401
468
|
meta: {
|
|
402
469
|
message: trimmedContent.slice(0, MAX_ASSISTANT_RESPONSE_LENGTH),
|
|
@@ -418,7 +485,7 @@ function extractTimelineEvents(transcript, fromIndex = 0) {
|
|
|
418
485
|
for (const toolUse of toolUses) {
|
|
419
486
|
events.push({
|
|
420
487
|
type: 'tool_use',
|
|
421
|
-
source:
|
|
488
|
+
source: 'cli_interactive',
|
|
422
489
|
timestamp,
|
|
423
490
|
meta: {
|
|
424
491
|
tool_name: toolUse.name,
|
|
@@ -448,7 +515,7 @@ function extractTimelineEvents(transcript, fromIndex = 0) {
|
|
|
448
515
|
|
|
449
516
|
events.push({
|
|
450
517
|
type: isError ? 'tool_failed' : 'tool_completed',
|
|
451
|
-
source:
|
|
518
|
+
source: 'cli_interactive',
|
|
452
519
|
timestamp,
|
|
453
520
|
meta: {
|
|
454
521
|
tool_use_id: toolResult.tool_use_id,
|
|
@@ -573,14 +640,22 @@ export async function ingestTranscriptToTimeline(options) {
|
|
|
573
640
|
let fromIndex = 0;
|
|
574
641
|
|
|
575
642
|
if (task_id) {
|
|
576
|
-
// For tasks: Use task's last ingested index
|
|
643
|
+
// For tasks: Use task's last ingested index, fall back to local cursor if fetchTask fails
|
|
577
644
|
const task = await fetchTask(task_id, parent_session_id, config);
|
|
578
|
-
|
|
579
|
-
|
|
645
|
+
if (task?.last_transcript_index != null) {
|
|
646
|
+
fromIndex = task.last_transcript_index;
|
|
647
|
+
console.log(`[transcript] Using task's last ingested index: ${fromIndex}`);
|
|
648
|
+
} else {
|
|
649
|
+
// fetchTask returned null (network error) or task has no index — use local cursor
|
|
650
|
+
// Use claude_session_id as cursor key for tasks (each task has unique child session)
|
|
651
|
+
fromIndex = await readCursor(claude_session_id);
|
|
652
|
+
console.log(`[transcript] Task fetch failed or no index — using local cursor: ${fromIndex}`);
|
|
653
|
+
}
|
|
580
654
|
} else {
|
|
581
|
-
// For regular sessions:
|
|
582
|
-
//
|
|
583
|
-
|
|
655
|
+
// For regular sessions: Use local cursor as primary deduplication,
|
|
656
|
+
// with timeline query as validation
|
|
657
|
+
const cursorMessageCount = await readCursor(parent_session_id);
|
|
658
|
+
console.log(`[transcript] Local cursor: ${cursorMessageCount} messages previously processed`);
|
|
584
659
|
console.log(`[transcript] Querying timeline for session ${parent_session_id} to find recent events...`);
|
|
585
660
|
try {
|
|
586
661
|
const timelineResponse = await fetch(
|
|
@@ -599,46 +674,69 @@ export async function ingestTranscriptToTimeline(options) {
|
|
|
599
674
|
console.log(`[transcript] Timeline returned ${timelineEvents.length} events`);
|
|
600
675
|
|
|
601
676
|
if (timelineEvents.length > 0 && timelineEvents[0].timestamp) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
fromIndex = transcript.findIndex(msg => {
|
|
615
|
-
const msgTimestamp = new Date(msg.timestamp || 0).getTime();
|
|
616
|
-
return msgTimestamp > lastTimestamp;
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// If not found, start from end of transcript (all messages are older)
|
|
620
|
-
if (fromIndex === -1) {
|
|
621
|
-
log(`All messages older than last timeline event (${new Date(lastTimestamp).toISOString()}) - no new events`);
|
|
622
|
-
console.log(`[transcript] All ${transcript.length} messages are older than last timeline event - no new events to ingest`);
|
|
623
|
-
fromIndex = transcript.length;
|
|
677
|
+
// Timeline API returns timestamps as string epoch ms (e.g. "1771211826406")
|
|
678
|
+
// Must parse as number — new Date("1771211826406") returns Invalid Date
|
|
679
|
+
const rawTimestamp = timelineEvents[0].timestamp;
|
|
680
|
+
const lastTimestamp = typeof rawTimestamp === 'string' && /^\d+$/.test(rawTimestamp)
|
|
681
|
+
? Number(rawTimestamp)
|
|
682
|
+
: typeof rawTimestamp === 'number'
|
|
683
|
+
? rawTimestamp
|
|
684
|
+
: new Date(rawTimestamp).getTime();
|
|
685
|
+
|
|
686
|
+
if (isNaN(lastTimestamp)) {
|
|
687
|
+
console.log(`[transcript] Could not parse timeline timestamp: ${rawTimestamp} - using local cursor`);
|
|
688
|
+
fromIndex = cursorMessageCount;
|
|
624
689
|
} else {
|
|
625
|
-
log(`
|
|
626
|
-
|
|
690
|
+
console.log(`[transcript] Last timeline event timestamp: ${lastTimestamp} (${new Date(lastTimestamp).toISOString()})`);
|
|
691
|
+
|
|
692
|
+
// Find the index of the first message after this timestamp
|
|
693
|
+
fromIndex = transcript.findIndex(msg => {
|
|
694
|
+
const msgTs = msg.timestamp;
|
|
695
|
+
// Transcript timestamps are ISO strings like "2026-02-15T16:10:42.035Z"
|
|
696
|
+
// or may be missing (undefined)
|
|
697
|
+
const msgTimestamp = !msgTs ? 0
|
|
698
|
+
: typeof msgTs === 'number' ? msgTs
|
|
699
|
+
: typeof msgTs === 'string' && /^\d+$/.test(msgTs) ? Number(msgTs)
|
|
700
|
+
: new Date(msgTs).getTime() || 0;
|
|
701
|
+
return msgTimestamp > lastTimestamp;
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// If not found, start from end of transcript (all messages are older)
|
|
705
|
+
if (fromIndex === -1) {
|
|
706
|
+
log(`All messages older than last timeline event (${new Date(lastTimestamp).toISOString()}) - no new events`);
|
|
707
|
+
console.log(`[transcript] All ${transcript.length} messages are older than last timeline event - no new events to ingest`);
|
|
708
|
+
fromIndex = transcript.length;
|
|
709
|
+
} else {
|
|
710
|
+
log(`Found ${transcript.length - fromIndex} new messages starting from index ${fromIndex}/${transcript.length}`);
|
|
711
|
+
console.log(`[transcript] Found ${transcript.length - fromIndex} new messages starting from index ${fromIndex}/${transcript.length}`);
|
|
712
|
+
}
|
|
627
713
|
}
|
|
628
714
|
} else {
|
|
629
|
-
|
|
630
|
-
fromIndex =
|
|
715
|
+
// No timeline events — use local cursor to avoid reprocessing
|
|
716
|
+
fromIndex = cursorMessageCount;
|
|
717
|
+
if (cursorMessageCount > 0) {
|
|
718
|
+
console.log(`[transcript] No timeline events found - using local cursor (${cursorMessageCount})`);
|
|
719
|
+
} else {
|
|
720
|
+
console.log(`[transcript] No timeline events and no cursor - processing all transcript messages`);
|
|
721
|
+
}
|
|
631
722
|
}
|
|
632
723
|
} else {
|
|
633
|
-
|
|
634
|
-
|
|
724
|
+
fromIndex = cursorMessageCount;
|
|
725
|
+
console.log(`[transcript] Timeline query failed (${timelineResponse.status}) - using local cursor (${cursorMessageCount})`);
|
|
635
726
|
}
|
|
636
727
|
} catch (error) {
|
|
637
|
-
|
|
638
|
-
|
|
728
|
+
fromIndex = cursorMessageCount;
|
|
729
|
+
console.log(`[transcript] Failed to query timeline: ${error.message} - using local cursor (${cursorMessageCount})`);
|
|
639
730
|
}
|
|
640
731
|
}
|
|
641
732
|
|
|
733
|
+
// Clamp fromIndex to transcript length to prevent stale cursors from
|
|
734
|
+
// skipping past the end of a shorter transcript (e.g., cursor=500, transcript has 100 messages)
|
|
735
|
+
if (fromIndex > transcript.length) {
|
|
736
|
+
console.warn(`[transcript] Clamping fromIndex from ${fromIndex} to transcript length ${transcript.length} — cursor may be stale or transcript truncated`);
|
|
737
|
+
fromIndex = transcript.length;
|
|
738
|
+
}
|
|
739
|
+
|
|
642
740
|
// 3. Extract only NEW events from determined index
|
|
643
741
|
const allEvents = extractTimelineEvents(transcript, fromIndex);
|
|
644
742
|
|
|
@@ -677,9 +775,16 @@ export async function ingestTranscriptToTimeline(options) {
|
|
|
677
775
|
log(`===== INGESTION COMPLETE: ${successCount}/${events.length} events pushed (${failCount} failed) =====`);
|
|
678
776
|
console.log(`[transcript] Pushed ${successCount}/${events.length} events (${failCount} failed)`);
|
|
679
777
|
|
|
680
|
-
// 6. Update
|
|
681
|
-
if (
|
|
682
|
-
|
|
778
|
+
// 6. Update cursors to prevent duplicates on next call
|
|
779
|
+
if (successCount > 0) {
|
|
780
|
+
// Local cursor — use claude_session_id for tasks (unique per child session),
|
|
781
|
+
// parent_session_id for regular sessions (maps to transcript file)
|
|
782
|
+
const cursorKey = task_id ? claude_session_id : parent_session_id;
|
|
783
|
+
await writeCursor(cursorKey, transcript.length, successCount);
|
|
784
|
+
// Remote cursor (tasks only) — persists in relay
|
|
785
|
+
if (task_id) {
|
|
786
|
+
await updateLastIngestedIndex(task_id, parent_session_id, transcript.length, config);
|
|
787
|
+
}
|
|
683
788
|
}
|
|
684
789
|
|
|
685
790
|
return { events_pushed: successCount, events_failed: failCount };
|
|
@@ -694,3 +799,6 @@ export async function getTranscriptLength(claude_session_id) {
|
|
|
694
799
|
const transcript = await readTranscript(claude_session_id);
|
|
695
800
|
return transcript.length;
|
|
696
801
|
}
|
|
802
|
+
|
|
803
|
+
// Export internals for unit testing
|
|
804
|
+
export { parseTimestamp, extractTimelineEvents, readCursor, writeCursor };
|
package/lib/install/installer.js
CHANGED
|
@@ -287,11 +287,11 @@ export async function installGeminiHooks(sourceGeminiHooksDir) {
|
|
|
287
287
|
settings.hooks = settings.hooks || {};
|
|
288
288
|
|
|
289
289
|
const hooksConfig = {
|
|
290
|
-
BeforeTool: [{ command: `
|
|
291
|
-
AfterTool: [{ command: `
|
|
292
|
-
SessionStart: [{ command: `
|
|
293
|
-
AfterAgent: [{ command: `
|
|
294
|
-
SessionEnd: [{ command: `
|
|
290
|
+
BeforeTool: [{ command: `bun ${join(destHooksDir, 'before_tool.mjs')}`, timeout: 65000 }],
|
|
291
|
+
AfterTool: [{ command: `bun ${join(destHooksDir, 'after_tool.mjs')}`, timeout: 10000 }],
|
|
292
|
+
SessionStart: [{ command: `bun ${join(destHooksDir, 'session_start.mjs')}`, timeout: 15000 }],
|
|
293
|
+
AfterAgent: [{ command: `bun ${join(destHooksDir, 'after_agent.mjs')}`, timeout: 15000 }],
|
|
294
|
+
SessionEnd: [{ command: `bun ${join(destHooksDir, 'session_end.mjs')}`, timeout: 10000 }]
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
// Standardize: Ensure BeforeAgent is also set if needed (parity with Claude's UserPromptSubmit)
|
|
@@ -316,7 +316,10 @@ export async function installDaemon() {
|
|
|
316
316
|
|
|
317
317
|
const daemonFiles = [
|
|
318
318
|
'teleportation-daemon.js',
|
|
319
|
-
'lifecycle.js'
|
|
319
|
+
'lifecycle.js',
|
|
320
|
+
'task-executor-v2.js',
|
|
321
|
+
'transcript-ingestion.js',
|
|
322
|
+
'timeline-analyzer.js',
|
|
320
323
|
];
|
|
321
324
|
|
|
322
325
|
const installed = [];
|
|
@@ -352,7 +355,7 @@ export async function installDaemon() {
|
|
|
352
355
|
|
|
353
356
|
/**
|
|
354
357
|
* Copy daemon dependency modules to ~/.teleportation/
|
|
355
|
-
* This includes machine-coders and
|
|
358
|
+
* This includes machine-coders, router, utils, and auth
|
|
356
359
|
*/
|
|
357
360
|
export async function installDaemonModules() {
|
|
358
361
|
const sourceLibDir = join(getTeleportationDir(), 'lib');
|
|
@@ -377,6 +380,19 @@ export async function installDaemonModules() {
|
|
|
377
380
|
'models.js',
|
|
378
381
|
'mech-llms-client.js'
|
|
379
382
|
]
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: 'utils',
|
|
386
|
+
files: [
|
|
387
|
+
'logger.js',
|
|
388
|
+
'log-sanitizer.js'
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'auth',
|
|
393
|
+
files: [
|
|
394
|
+
'credentials.js'
|
|
395
|
+
]
|
|
380
396
|
}
|
|
381
397
|
];
|
|
382
398
|
|
|
@@ -513,42 +529,56 @@ export async function createSettings() {
|
|
|
513
529
|
matcher: ".*",
|
|
514
530
|
hooks: [{
|
|
515
531
|
type: "command",
|
|
516
|
-
command: `
|
|
532
|
+
command: `bun ${join(projectHooksDir, 'pre_tool_use.mjs')}`
|
|
517
533
|
}]
|
|
518
534
|
}],
|
|
519
535
|
Stop: [{
|
|
520
536
|
matcher: ".*",
|
|
521
537
|
hooks: [{
|
|
522
538
|
type: "command",
|
|
523
|
-
command: `
|
|
539
|
+
command: `bun ${join(projectHooksDir, 'stop.mjs')}`
|
|
524
540
|
}]
|
|
525
541
|
}],
|
|
526
542
|
SessionStart: [{
|
|
527
543
|
matcher: ".*",
|
|
528
544
|
hooks: [{
|
|
529
545
|
type: "command",
|
|
530
|
-
command: `
|
|
546
|
+
command: `bun ${join(projectHooksDir, 'session_start.mjs')}`
|
|
531
547
|
}]
|
|
532
548
|
}],
|
|
533
549
|
SessionEnd: [{
|
|
534
550
|
matcher: ".*",
|
|
535
551
|
hooks: [{
|
|
536
552
|
type: "command",
|
|
537
|
-
command: `
|
|
553
|
+
command: `bun ${join(projectHooksDir, 'session_end.mjs')}`
|
|
538
554
|
}]
|
|
539
555
|
}],
|
|
540
556
|
Notification: [{
|
|
541
557
|
matcher: ".*",
|
|
542
558
|
hooks: [{
|
|
543
559
|
type: "command",
|
|
544
|
-
command: `
|
|
560
|
+
command: `bun ${join(projectHooksDir, 'notification.mjs')}`
|
|
545
561
|
}]
|
|
546
562
|
}],
|
|
547
563
|
UserPromptSubmit: [{
|
|
548
564
|
matcher: ".*",
|
|
549
565
|
hooks: [{
|
|
550
566
|
type: "command",
|
|
551
|
-
command: `
|
|
567
|
+
command: `bun ${join(projectHooksDir, 'user_prompt_submit.mjs')}`
|
|
568
|
+
}]
|
|
569
|
+
}],
|
|
570
|
+
PermissionRequest: [{
|
|
571
|
+
matcher: ".*",
|
|
572
|
+
hooks: [{
|
|
573
|
+
type: "command",
|
|
574
|
+
command: `bun ${join(projectHooksDir, 'permission_request.mjs')}`
|
|
575
|
+
}]
|
|
576
|
+
}],
|
|
577
|
+
PostToolUse: [{
|
|
578
|
+
matcher: ".*",
|
|
579
|
+
hooks: [{
|
|
580
|
+
type: "command",
|
|
581
|
+
command: `bun ${join(projectHooksDir, 'post_tool_use.mjs')}`
|
|
552
582
|
}]
|
|
553
583
|
}]
|
|
554
584
|
}
|
package/package.json
CHANGED
package/teleportation-cli.cjs
CHANGED
|
@@ -1295,7 +1295,63 @@ async function commandTest() {
|
|
|
1295
1295
|
console.log(c.red(' ❌ FAIL - Hook execution error\n'));
|
|
1296
1296
|
failed++;
|
|
1297
1297
|
}
|
|
1298
|
-
|
|
1298
|
+
|
|
1299
|
+
// Test 6: Approval Round-Trip
|
|
1300
|
+
console.log(c.yellow('Test 6: Approval Round-Trip'));
|
|
1301
|
+
if (creds.RELAY_API_KEY && relayUrl) {
|
|
1302
|
+
try {
|
|
1303
|
+
// Create a test approval
|
|
1304
|
+
const createRes = await fetch(`${relayUrl}/api/approvals`, {
|
|
1305
|
+
method: 'POST',
|
|
1306
|
+
headers: {
|
|
1307
|
+
'Content-Type': 'application/json',
|
|
1308
|
+
'Authorization': `Bearer ${creds.RELAY_API_KEY}`
|
|
1309
|
+
},
|
|
1310
|
+
body: JSON.stringify({
|
|
1311
|
+
session_id: 'test-smoke-' + Date.now(),
|
|
1312
|
+
tool_name: 'Read',
|
|
1313
|
+
tool_input: { file_path: '/tmp/smoke-test.txt' }
|
|
1314
|
+
}),
|
|
1315
|
+
signal: AbortSignal.timeout(10000)
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
if (!createRes.ok) throw new Error(`Create failed: ${createRes.status}`);
|
|
1319
|
+
const { id: approvalId } = await createRes.json();
|
|
1320
|
+
|
|
1321
|
+
// Approve it
|
|
1322
|
+
const decideRes = await fetch(`${relayUrl}/api/approvals/${approvalId}/decision`, {
|
|
1323
|
+
method: 'POST',
|
|
1324
|
+
headers: {
|
|
1325
|
+
'Content-Type': 'application/json',
|
|
1326
|
+
'Authorization': `Bearer ${creds.RELAY_API_KEY}`
|
|
1327
|
+
},
|
|
1328
|
+
body: JSON.stringify({ decision: 'allow', reason: 'smoke test' }),
|
|
1329
|
+
signal: AbortSignal.timeout(10000)
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
if (!decideRes.ok) throw new Error(`Decide failed: ${decideRes.status}`);
|
|
1333
|
+
|
|
1334
|
+
// Verify
|
|
1335
|
+
const checkRes = await fetch(`${relayUrl}/api/approvals/${approvalId}`, {
|
|
1336
|
+
headers: { 'Authorization': `Bearer ${creds.RELAY_API_KEY}` },
|
|
1337
|
+
signal: AbortSignal.timeout(10000)
|
|
1338
|
+
});
|
|
1339
|
+
const approval = await checkRes.json();
|
|
1340
|
+
|
|
1341
|
+
if (approval.status === 'allowed') {
|
|
1342
|
+
console.log(c.green(' ✅ PASS - Approval round-trip works\n'));
|
|
1343
|
+
passed++;
|
|
1344
|
+
} else {
|
|
1345
|
+
throw new Error(`Expected status=allowed, got ${approval.status}`);
|
|
1346
|
+
}
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
console.log(c.red(` ❌ FAIL - Approval round-trip: ${e.message}\n`));
|
|
1349
|
+
failed++;
|
|
1350
|
+
}
|
|
1351
|
+
} else {
|
|
1352
|
+
console.log(c.yellow(' ⏭️ SKIP - No credentials for round-trip test\n'));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1299
1355
|
// Summary
|
|
1300
1356
|
console.log(c.purple('Test Summary:'));
|
|
1301
1357
|
console.log(` Passed: ${c.green(passed)}`);
|