@kraki/tentacle 0.11.20 → 0.12.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/banner.js CHANGED
@@ -7,7 +7,7 @@ import bannerData from './banner-data.json' with { type: 'json' };
7
7
  const data = bannerData;
8
8
  const SCRAMBLE = '!@#$%^&*=+<>~/';
9
9
  const TITLE = 'KRAKI';
10
- const TITLE_COLORS = ['#00c9a7', '#00b4d8', '#06b6d4', '#ea6046', '#0891b2'];
10
+ const TITLE_COLORS = ['#00c9a7', '#00b4d8', '#ea6046', '#0891b2', '#ea6046'];
11
11
  const BLOCK_MAP = {
12
12
  '.': '░', ':': '░', '-': '▒', '=': '▓', '+': '█', '*': '█', '#': '█', '%': '█', '@': '█',
13
13
  };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * History parser — converts Copilot SDK events.jsonl into Kraki protocol messages.
3
+ *
4
+ * Used during import to backfill conversation history so the arm can display
5
+ * the full session context from before Kraki attached.
6
+ *
7
+ * Reads events.jsonl line by line (streaming) to handle large files.
8
+ * Caps output at MAX_BACKFILL_MESSAGES most recent messages.
9
+ */
10
+ export interface BackfilledMessage {
11
+ seq: number;
12
+ type: string;
13
+ payload: string;
14
+ ts: string;
15
+ }
16
+ export interface ParsedSessionMeta {
17
+ model?: string;
18
+ cwd?: string;
19
+ gitRoot?: string;
20
+ branch?: string;
21
+ repository?: string;
22
+ }
23
+ /**
24
+ * Parse a Copilot SDK events.jsonl file into Kraki protocol messages.
25
+ * Returns up to MAX_BACKFILL_MESSAGES most recent messages and session metadata.
26
+ */
27
+ export declare function parseEventsFile(eventsPath: string): {
28
+ messages: BackfilledMessage[];
29
+ meta: ParsedSessionMeta;
30
+ };
31
+ /**
32
+ * Parse events.jsonl from a session directory.
33
+ */
34
+ export declare function parseSessionHistory(sessionDir: string): {
35
+ messages: BackfilledMessage[];
36
+ meta: ParsedSessionMeta;
37
+ };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * History parser — converts Copilot SDK events.jsonl into Kraki protocol messages.
3
+ *
4
+ * Used during import to backfill conversation history so the arm can display
5
+ * the full session context from before Kraki attached.
6
+ *
7
+ * Reads events.jsonl line by line (streaming) to handle large files.
8
+ * Caps output at MAX_BACKFILL_MESSAGES most recent messages.
9
+ */
10
+ import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { createLogger } from './logger.js';
13
+ const logger = createLogger('history-parser');
14
+ /** Max messages to backfill from events.jsonl (most recent kept). */
15
+ const MAX_BACKFILL_MESSAGES = 500;
16
+ // ── Parser ──────────────────────────────────────────────
17
+ /**
18
+ * Parse a Copilot SDK events.jsonl file into Kraki protocol messages.
19
+ * Returns up to MAX_BACKFILL_MESSAGES most recent messages and session metadata.
20
+ */
21
+ export function parseEventsFile(eventsPath) {
22
+ if (!existsSync(eventsPath)) {
23
+ return { messages: [], meta: {} };
24
+ }
25
+ const messages = [];
26
+ const meta = {};
27
+ let seq = 0;
28
+ // Stream line by line using a buffer to handle large files
29
+ const content = readFileChunked(eventsPath);
30
+ for (const line of content.split('\n')) {
31
+ if (!line.trim())
32
+ continue;
33
+ let event;
34
+ try {
35
+ event = JSON.parse(line);
36
+ }
37
+ catch {
38
+ continue; // skip corrupt lines
39
+ }
40
+ const ts = event.timestamp ?? new Date().toISOString();
41
+ const converted = convertEvent(event, ts, meta);
42
+ if (converted) {
43
+ seq++;
44
+ messages.push({ seq, ...converted });
45
+ }
46
+ }
47
+ // Keep only the most recent messages
48
+ if (messages.length > MAX_BACKFILL_MESSAGES) {
49
+ const trimmed = messages.slice(messages.length - MAX_BACKFILL_MESSAGES);
50
+ // Re-number seq starting from 1
51
+ for (let i = 0; i < trimmed.length; i++) {
52
+ trimmed[i].seq = i + 1;
53
+ }
54
+ return { messages: trimmed, meta };
55
+ }
56
+ return { messages, meta };
57
+ }
58
+ /**
59
+ * Parse events.jsonl from a session directory.
60
+ */
61
+ export function parseSessionHistory(sessionDir) {
62
+ return parseEventsFile(join(sessionDir, 'events.jsonl'));
63
+ }
64
+ // ── Event conversion ────────────────────────────────────
65
+ function convertEvent(event, ts, meta) {
66
+ switch (event.type) {
67
+ case 'session.start': {
68
+ // Extract metadata, don't emit a message
69
+ const ctx = event.data.context;
70
+ if (event.data.selectedModel)
71
+ meta.model = event.data.selectedModel;
72
+ if (ctx?.cwd)
73
+ meta.cwd = ctx.cwd;
74
+ if (ctx?.gitRoot)
75
+ meta.gitRoot = ctx.gitRoot;
76
+ if (ctx?.branch)
77
+ meta.branch = ctx.branch;
78
+ if (ctx?.repository)
79
+ meta.repository = ctx.repository;
80
+ return null;
81
+ }
82
+ case 'user.message': {
83
+ const content = event.data.content ?? '';
84
+ // Use transformedContent if available (has system context stripped)
85
+ return {
86
+ type: 'user_message',
87
+ payload: JSON.stringify({ content }),
88
+ ts,
89
+ };
90
+ }
91
+ case 'assistant.message': {
92
+ const content = event.data.content ?? '';
93
+ if (!content)
94
+ return null; // skip empty messages (SDK sends these before tool calls)
95
+ return {
96
+ type: 'agent_message',
97
+ payload: JSON.stringify({ content }),
98
+ ts,
99
+ };
100
+ }
101
+ case 'tool.execution_start': {
102
+ const toolName = event.data.toolName ?? 'unknown';
103
+ const args = (event.data.arguments ?? event.data.args ?? {});
104
+ const toolCallId = event.data.toolCallId;
105
+ return {
106
+ type: 'tool_start',
107
+ payload: JSON.stringify({ toolName, args, toolCallId }),
108
+ ts,
109
+ };
110
+ }
111
+ case 'tool.execution_complete': {
112
+ const toolName = event.data.toolName ?? 'unknown';
113
+ const toolCallId = event.data.toolCallId;
114
+ const success = event.data.success;
115
+ const rawResult = event.data.result;
116
+ const resultObj = typeof rawResult === 'object' && rawResult !== null
117
+ ? rawResult
118
+ : null;
119
+ const result = resultObj?.content
120
+ ?? (typeof rawResult === 'string' ? rawResult : (event.data.output ?? ''));
121
+ // Extract model from the first tool completion that has it
122
+ if (!meta.model && event.data.model) {
123
+ meta.model = event.data.model;
124
+ }
125
+ return {
126
+ type: 'tool_complete',
127
+ payload: JSON.stringify({
128
+ toolName,
129
+ args: {},
130
+ result: result.slice(0, 5000), // Cap result size for backfill
131
+ toolCallId,
132
+ success,
133
+ }),
134
+ ts,
135
+ };
136
+ }
137
+ case 'assistant.turn_end': {
138
+ return {
139
+ type: 'idle',
140
+ payload: JSON.stringify({}),
141
+ ts,
142
+ };
143
+ }
144
+ // Skip events that don't map to Kraki messages
145
+ case 'assistant.message_delta':
146
+ case 'assistant.turn_start':
147
+ case 'hook.start':
148
+ case 'hook.end':
149
+ case 'subagent.started':
150
+ case 'session.idle':
151
+ case 'session.shutdown':
152
+ case 'session.info':
153
+ case 'session.warning':
154
+ case 'session.task_complete':
155
+ case 'assistant.usage':
156
+ case 'session.title_changed':
157
+ return null;
158
+ default:
159
+ // Unknown event type — skip
160
+ return null;
161
+ }
162
+ }
163
+ // ── File reading helper ─────────────────────────────────
164
+ function readFileChunked(filePath) {
165
+ // For files under 10MB, just read the whole thing
166
+ const stat = statSync(filePath);
167
+ if (stat.size <= 10 * 1024 * 1024) {
168
+ const fd = openSync(filePath, 'r');
169
+ const buf = Buffer.alloc(stat.size);
170
+ readSync(fd, buf, 0, stat.size, 0);
171
+ closeSync(fd);
172
+ return buf.toString('utf8');
173
+ }
174
+ // For larger files, read in chunks
175
+ logger.info({ filePath, size: stat.size }, 'Large events.jsonl — reading in chunks');
176
+ const fd = openSync(filePath, 'r');
177
+ const chunks = [];
178
+ const chunkSize = 1024 * 1024; // 1MB chunks
179
+ let offset = 0;
180
+ while (offset < stat.size) {
181
+ const readSize = Math.min(chunkSize, stat.size - offset);
182
+ const buf = Buffer.alloc(readSize);
183
+ readSync(fd, buf, 0, readSize, offset);
184
+ chunks.push(buf);
185
+ offset += readSize;
186
+ }
187
+ closeSync(fd);
188
+ return Buffer.concat(chunks).toString('utf8');
189
+ }
190
+ //# sourceMappingURL=history-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-parser.js","sourceRoot":"","sources":["../src/history-parser.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,7 +2,11 @@ export { AgentAdapter, CopilotAdapter, parsePermission } from './adapters/index.
2
2
  export type { CreateSessionConfig, SessionInfo, PermissionDecision, SessionCreatedEvent, MessageEvent, MessageDeltaEvent, PermissionRequestEvent, QuestionRequestEvent, ToolStartEvent, ToolCompleteEvent, SessionEndedEvent, ErrorEvent, ParsedPermission, } from './adapters/index.js';
3
3
  export { loadConfig, saveConfig, configExists, getOrCreateDeviceId } from './config.js';
4
4
  export { SessionManager } from './session-manager.js';
5
- export type { SessionContext, SessionMeta, RunRecord, LoggedMessage } from './session-manager.js';
5
+ export type { SessionContext, SessionMeta, RunRecord, LoggedMessage, SessionLink } from './session-manager.js';
6
6
  export { RelayClient } from './relay-client.js';
7
7
  export type { RelayClientOptions, RelayClientState } from './relay-client.js';
8
8
  export { KeyManager } from './key-manager.js';
9
+ export { scanLocalSessions, filterSessions } from './session-scanner.js';
10
+ export type { ScanOptions, SessionFilter } from './session-scanner.js';
11
+ export { parseEventsFile, parseSessionHistory } from './history-parser.js';
12
+ export type { BackfilledMessage, ParsedSessionMeta } from './history-parser.js';
package/dist/index.js CHANGED
@@ -8,4 +8,6 @@ export { loadConfig, saveConfig, configExists, getOrCreateDeviceId } from './con
8
8
  export { SessionManager } from './session-manager.js';
9
9
  export { RelayClient } from './relay-client.js';
10
10
  export { KeyManager } from './key-manager.js';
11
+ export { scanLocalSessions, filterSessions } from './session-scanner.js';
12
+ export { parseEventsFile, parseSessionHistory } from './history-parser.js';
11
13
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,EAAE;AACF,0DAA0D;AAC1D,+DAA+D;AAC/D,0CAA0C;AAE1C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAkBpF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,EAAE;AACF,0DAA0D;AAC1D,+DAA+D;AAC/D,0CAA0C;AAE1C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAkBpF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -90,6 +90,8 @@ export declare class RelayClient {
90
90
  private handleConsumerMessage;
91
91
  private handleCreateSession;
92
92
  private handleForkSession;
93
+ private handleRequestLocalSessions;
94
+ private handleImportSession;
93
95
  /**
94
96
  * Process queued unicast envelopes delivered by the relay in auth_ok.
95
97
  * These are messages sent by arms while this tentacle was offline.
@@ -8,7 +8,10 @@
8
8
  import { WebSocket } from 'ws';
9
9
  import { appendFileSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
11
12
  import { importPublicKey, encryptToBlob, decryptFromBlob, signChallenge } from '@kraki/crypto';
13
+ import { scanLocalSessions, filterSessions } from './session-scanner.js';
14
+ import { parseSessionHistory } from './history-parser.js';
12
15
  import { createLogger } from './logger.js';
13
16
  import { getKrakiHome } from './config.js';
14
17
  const logger = createLogger('relay-client');
@@ -310,6 +313,15 @@ export class RelayClient {
310
313
  this.handleClientLog(msg.deviceId, payload?.entries);
311
314
  return;
312
315
  }
316
+ // ── Local session sync (no sessionId) ────────────────
317
+ if (msg.type === 'request_local_sessions') {
318
+ this.handleRequestLocalSessions(msg);
319
+ return;
320
+ }
321
+ if (msg.type === 'import_session') {
322
+ this.handleImportSession(msg);
323
+ return;
324
+ }
313
325
  const sessionId = msg.sessionId;
314
326
  if (!sessionId)
315
327
  return;
@@ -366,6 +378,7 @@ export class RelayClient {
366
378
  this.adapter.killSession(sessionId)
367
379
  .catch((err) => logger.error({ err, sessionId }, 'killSession on delete failed'))
368
380
  .finally(() => {
381
+ this.sessionManager.removeLinkByKrakiId(sessionId);
369
382
  this.sessionManager.deleteSession(sessionId);
370
383
  this.lastAgentContent.delete(sessionId);
371
384
  this.send({ type: 'session_deleted', sessionId, payload: {} });
@@ -512,6 +525,154 @@ export class RelayClient {
512
525
  });
513
526
  }
514
527
  }
528
+ // ── Local session sync handlers ───────────────────────
529
+ handleRequestLocalSessions(msg) {
530
+ if (msg.type !== 'request_local_sessions')
531
+ return;
532
+ const { requestId, filter } = msg.payload;
533
+ const requesterDeviceId = msg.deviceId;
534
+ const requesterKey = this.consumerKeys.get(requesterDeviceId);
535
+ try {
536
+ let sessions = scanLocalSessions();
537
+ const linkedIds = this.sessionManager.getLinkedIds();
538
+ // Mark sessions that are already linked
539
+ for (const s of sessions) {
540
+ const link = this.sessionManager.getLink(s.sessionId);
541
+ if (link)
542
+ s.linkedKrakiSessionId = link.krakiSessionId;
543
+ }
544
+ // Exclude sessions that Kraki already manages (created natively, not imported)
545
+ const krakiSessionIds = new Set(this.sessionManager.getSessionList().map(s => s.id));
546
+ sessions = sessions.filter(s => !krakiSessionIds.has(s.sessionId) || s.linkedKrakiSessionId);
547
+ // Apply filters
548
+ if (filter) {
549
+ sessions = filterSessions(sessions, filter, linkedIds);
550
+ }
551
+ const response = {
552
+ type: 'local_sessions_list',
553
+ deviceId: this.authInfo?.deviceId ?? '',
554
+ seq: ++this.seqCounter,
555
+ timestamp: new Date().toISOString(),
556
+ payload: { sessions, requestId },
557
+ };
558
+ if (requesterKey) {
559
+ this.sendUnicastTo(requesterDeviceId, requesterKey, response);
560
+ }
561
+ else {
562
+ // No encryption key — broadcast (works in open/non-E2E mode)
563
+ this.send(response);
564
+ }
565
+ logger.debug({ count: sessions.length, requestId }, 'Sent local sessions list');
566
+ }
567
+ catch (err) {
568
+ logger.error({ err }, 'Failed to scan local sessions');
569
+ const response = {
570
+ type: 'local_sessions_list',
571
+ deviceId: this.authInfo?.deviceId ?? '',
572
+ seq: ++this.seqCounter,
573
+ timestamp: new Date().toISOString(),
574
+ payload: { sessions: [], requestId },
575
+ };
576
+ if (requesterKey) {
577
+ this.sendUnicastTo(requesterDeviceId, requesterKey, response);
578
+ }
579
+ else {
580
+ this.send(response);
581
+ }
582
+ }
583
+ }
584
+ async handleImportSession(msg) {
585
+ if (msg.type !== 'import_session')
586
+ return;
587
+ const { requestId, localSessionId } = msg.payload;
588
+ // Check if already linked
589
+ const existing = this.sessionManager.getLink(localSessionId);
590
+ if (existing) {
591
+ this.send({
592
+ type: 'error',
593
+ sessionId: '',
594
+ payload: { message: `Session already imported as ${existing.krakiSessionId} (requestId: ${requestId})` },
595
+ });
596
+ return;
597
+ }
598
+ try {
599
+ // Use localSessionId as Kraki session ID (shared identity)
600
+ const krakiSessionId = localSessionId;
601
+ // Parse events.jsonl for backfill
602
+ const sessionStateDir = join(homedir(), '.copilot', 'session-state', localSessionId);
603
+ const { messages: backfilledMessages, meta: parsedMeta } = parseSessionHistory(sessionStateDir);
604
+ // Scan to get local session metadata
605
+ const scanResults = scanLocalSessions();
606
+ const localSession = scanResults.find(s => s.sessionId === localSessionId);
607
+ // Create Kraki session
608
+ this.sessionManager.createSession('copilot', parsedMeta.model ?? localSession?.model, krakiSessionId);
609
+ // Persist source, title, model, and original creation time
610
+ this.sessionManager.updateMeta(krakiSessionId, {
611
+ source: localSession?.source ?? 'copilot-cli',
612
+ autoTitle: localSession?.summary?.slice(0, 100),
613
+ model: parsedMeta.model ?? localSession?.model,
614
+ createdAt: localSession?.startTime,
615
+ });
616
+ // Backfill messages
617
+ for (const m of backfilledMessages) {
618
+ this.sessionManager.appendMessage(krakiSessionId, m.type, m.payload);
619
+ }
620
+ // Write link table entry
621
+ this.sessionManager.addLink({
622
+ localSessionId,
623
+ krakiSessionId,
624
+ source: localSession?.source ?? 'copilot-cli',
625
+ cwd: localSession?.cwd,
626
+ branch: localSession?.branch,
627
+ linkedAt: new Date().toISOString(),
628
+ });
629
+ // Map requestId for session_created correlation
630
+ if (requestId) {
631
+ this.pendingRequestIds.set(krakiSessionId, requestId);
632
+ }
633
+ // Resume the session via SDK. Use createSession with the existing sessionId
634
+ // so the CLI server discovers the state on disk. resumeSession only works
635
+ // for sessions the CLI server has already loaded in memory.
636
+ const cwd = localSession?.cwd ?? parsedMeta.cwd ?? '/';
637
+ try {
638
+ await this.adapter.createSession({
639
+ sessionId: krakiSessionId,
640
+ model: parsedMeta.model,
641
+ cwd,
642
+ });
643
+ }
644
+ catch (createErr) {
645
+ // SDK resume failed — still keep backfilled history, but manually
646
+ // send session_created so the web UI knows the session exists.
647
+ logger.warn({ err: createErr.message, krakiSessionId }, 'SDK resume failed — session imported as idle');
648
+ const meta = this.sessionManager.getMeta(krakiSessionId);
649
+ this.send({
650
+ type: 'session_created',
651
+ sessionId: krakiSessionId,
652
+ payload: {
653
+ agent: 'copilot',
654
+ model: parsedMeta.model ?? localSession?.model,
655
+ requestId,
656
+ lastSeq: meta?.lastSeq ?? 0,
657
+ },
658
+ });
659
+ if (requestId)
660
+ this.pendingRequestIds.delete(krakiSessionId);
661
+ }
662
+ // Mark idle — will transition to active when user sends a message
663
+ this.sessionManager.markIdle(krakiSessionId);
664
+ this.send({ type: 'idle', sessionId: krakiSessionId, payload: {} });
665
+ logger.info({ localSessionId, krakiSessionId, backfilled: backfilledMessages.length }, 'Session imported');
666
+ }
667
+ catch (err) {
668
+ logger.error({ err, localSessionId }, 'Import session failed');
669
+ this.send({
670
+ type: 'error',
671
+ sessionId: '',
672
+ payload: { message: `Failed to import session: ${err.message} (requestId: ${requestId})` },
673
+ });
674
+ }
675
+ }
515
676
  // ── Pending message processing ─────────────────────
516
677
  /**
517
678
  * Process queued unicast envelopes delivered by the relay in auth_ok.