kiku-agent 1.0.10 → 1.0.11

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.
@@ -1,182 +1,234 @@
1
- import { spawnSync } from 'node:child_process';
2
-
3
- const DETAIL_VIEWS = new Set(['overview', 'answer', 'thinking', 'tools', 'tool', 'all', 'markdown']);
4
-
5
- export function parseDetailsRequest(reference = '') {
6
- const tokens = String(reference || '').trim().split(/\s+/).filter(Boolean);
7
- let queryNumber = null;
8
-
9
- if (tokens[0] && /^q?\d+$/i.test(tokens[0])) {
10
- queryNumber = Number.parseInt(tokens.shift().replace(/^q/i, ''), 10);
11
- } else if (tokens[0]?.toLowerCase() === 'latest') {
12
- tokens.shift();
13
- }
14
-
15
- const view = (tokens.shift() || 'answer').toLowerCase();
16
- if (!DETAIL_VIEWS.has(view)) {
17
- return { error: `Unknown details view "${view}".`, queryNumber, view: 'overview', toolIndex: null };
18
- }
19
-
20
- let toolIndex = null;
21
- if (view === 'tool') {
22
- const rawIndex = tokens.shift();
23
- if (!rawIndex || !/^\d+$/.test(rawIndex) || Number(rawIndex) < 1) {
24
- return { error: 'Tool view requires a positive tool number.', queryNumber, view, toolIndex: null };
25
- }
26
- toolIndex = Number(rawIndex);
27
- }
28
-
29
- if (tokens.length > 0) {
30
- return { error: 'Too many details arguments.', queryNumber, view, toolIndex };
31
- }
32
-
33
- return { error: null, queryNumber, view, toolIndex };
34
- }
35
-
36
- export function classifyToolResult(result = {}) {
37
- const count = result?.count ?? result?.returnedCount;
38
- if (result?.status === 'error') return { state: 'error', count };
39
- if (result?.status === 'not_found' || count === 0) return { state: 'not_found', count };
40
- return { state: 'ok', count };
41
- }
42
-
43
- export function summarizeQueryDetails(details = {}) {
44
- const tools = Array.isArray(details.tools) ? details.tools : [];
45
- const toolStates = tools.map(tool => classifyToolResult(tool.result));
46
- return {
47
- toolCount: tools.length,
48
- successfulTools: toolStates.filter(item => item.state === 'ok').length,
49
- failedTools: toolStates.filter(item => item.state === 'error').length,
50
- emptyTools: toolStates.filter(item => item.state === 'not_found').length,
51
- toolDurationMs: tools.reduce((total, tool) => total + (Number(tool.duration) || 0), 0),
52
- thinkingCharacters: String(details.thinking || '').length,
53
- answerCharacters: String(details.answer || '').length,
54
- };
55
- }
56
-
57
- export function formatDetailDuration(durationMs) {
58
- const duration = Number(durationMs);
59
- if (!Number.isFinite(duration) || duration < 0) return 'n/a';
60
- if (duration < 1000) return `${Math.round(duration)}ms`;
61
- return `${(duration / 1000).toFixed(duration < 10000 ? 2 : 1)}s`;
62
- }
63
-
64
- export function serializeDetailValue(value, maxLength = Infinity) {
65
- let serialized;
66
- try {
67
- serialized = JSON.stringify(value ?? {}, null, 2);
68
- } catch {
69
- serialized = String(value ?? '');
70
- }
71
- if (!Number.isFinite(maxLength) || serialized.length <= maxLength) {
72
- return { text: serialized, truncated: false };
73
- }
74
- return { text: `${serialized.slice(0, maxLength)}\n… result truncated`, truncated: true };
75
- }
76
-
77
- export function copyToClipboard(text) {
78
- if (typeof text !== 'string') text = String(text ?? '');
79
- const platform = process.platform;
80
-
81
- try {
82
- if (platform === 'win32') {
83
- const proc = spawnSync('clip', [], { input: text, encoding: 'utf-8', windowsHide: true });
84
- if (proc.status === 0) return true;
85
- const ps = spawnSync('powershell.exe', ['-NoProfile', '-Command', '$input | Set-Clipboard'], {
86
- input: text,
87
- encoding: 'utf-8',
88
- windowsHide: true,
89
- });
90
- return ps.status === 0;
91
- }
92
- if (platform === 'darwin') {
93
- const proc = spawnSync('pbcopy', [], { input: text, encoding: 'utf-8' });
94
- return proc.status === 0;
95
- }
96
- const tools = [
97
- { cmd: 'wl-copy', args: [] },
98
- { cmd: 'xclip', args: ['-selection', 'clipboard'] },
99
- { cmd: 'xsel', args: ['--clipboard', '--input'] },
100
- ];
101
- for (const t of tools) {
102
- try {
103
- const proc = spawnSync(t.cmd, t.args, { input: text, encoding: 'utf-8' });
104
- if (proc.status === 0) return true;
105
- } catch {}
106
- }
107
- } catch {}
108
- return false;
109
- }
110
-
111
- export function getCopyableContent(details, request = {}) {
112
- if (!details) return null;
113
- const view = request.view || 'answer';
114
- const queryLabel = `Q${details.number}`;
115
-
116
- if (view === 'answer') {
117
- return {
118
- title: `Answer • ${queryLabel}`,
119
- text: details.answer || '',
120
- type: 'answer',
121
- };
122
- }
123
-
124
- if (view === 'thinking') {
125
- return {
126
- title: `Thinking Trace • ${queryLabel}`,
127
- text: details.thinking || '',
128
- type: 'thinking',
129
- };
130
- }
131
-
132
- if (view === 'tool') {
133
- const tool = details.tools?.[request.toolIndex - 1];
134
- if (!tool) return null;
135
- return {
136
- title: `Tool #${request.toolIndex} (${tool.name}) • ${queryLabel}`,
137
- text: JSON.stringify({ name: tool.name, duration: tool.duration, args: tool.args, result: tool.result }, null, 2),
138
- type: 'tool',
139
- };
140
- }
141
-
142
- if (view === 'tools') {
143
- const serialized = JSON.stringify(details.tools || [], null, 2);
144
- return {
145
- title: `Tool Timeline • ${queryLabel}`,
146
- text: serialized,
147
- type: 'tools',
148
- };
149
- }
150
-
151
- if (view === 'markdown' || view === 'all') {
152
- const md = [
153
- `# Question ${details.number}`,
154
- `**Status**: ${details.status || 'complete'}`,
155
- `**Started**: ${Number.isFinite(details.startedAt) ? new Date(details.startedAt).toISOString() : 'n/a'}`,
156
- '',
157
- ...(details.thinking ? ['## Thinking', '```', details.thinking, '```', ''] : []),
158
- ...(details.tools?.length ? ['## Tools Used', '```json', JSON.stringify(details.tools, null, 2), '```', ''] : []),
159
- '## Answer',
160
- details.answer || '',
161
- ].join('\n');
162
- return {
163
- title: `Complete Query Markdown • ${queryLabel}`,
164
- text: md,
165
- type: 'markdown',
166
- };
167
- }
168
-
169
- const summary = summarizeQueryDetails(details);
170
- const overviewText = [
171
- `Query: Q${details.number}`,
172
- `Status: ${details.status}`,
173
- `Answer (${summary.answerCharacters} chars):\n${details.answer || ''}`,
174
- details.thinking ? `\nThinking (${summary.thinkingCharacters} chars):\n${details.thinking}` : '',
175
- ].filter(Boolean).join('\n');
176
-
177
- return {
178
- title: `Overview • ${queryLabel}`,
179
- text: overviewText,
180
- type: 'overview',
181
- };
182
- }
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ const DETAIL_VIEWS = new Set([
4
+ 'overview', 'answer', 'thinking', 'tools', 'tool', 'all',
5
+ 'markdown', 'json', 'text', 'raw', 'session', 'transcript', 'history'
6
+ ]);
7
+
8
+ export function parseDetailsRequest(reference = '') {
9
+ const tokens = String(reference || '').trim().split(/\s+/).filter(Boolean);
10
+ let queryNumber = null;
11
+ let isSession = false;
12
+
13
+ const first = (tokens[0] || '').toLowerCase();
14
+ if (first === 'session' || first === 'transcript' || first === 'history') {
15
+ isSession = true;
16
+ tokens.shift();
17
+ } else if (/^q?\d+$/i.test(tokens[0])) {
18
+ queryNumber = Number.parseInt(tokens.shift().replace(/^q/i, ''), 10);
19
+ } else if (tokens[0]?.toLowerCase() === 'latest') {
20
+ tokens.shift();
21
+ }
22
+
23
+ let view = (tokens.shift() || (isSession ? 'markdown' : 'answer')).toLowerCase();
24
+ if (view === 'session' || view === 'transcript' || view === 'history') {
25
+ isSession = true;
26
+ view = (tokens.shift() || 'markdown').toLowerCase();
27
+ }
28
+
29
+ if (!DETAIL_VIEWS.has(view)) {
30
+ return { error: `Unknown details view "${view}".`, queryNumber, view: 'overview', toolIndex: null, isSession };
31
+ }
32
+
33
+ let toolIndex = null;
34
+ if (view === 'tool') {
35
+ const rawIndex = tokens.shift();
36
+ if (!rawIndex || !/^\d+$/.test(rawIndex) || Number(rawIndex) < 1) {
37
+ return { error: 'Tool view requires a positive tool number.', queryNumber, view, toolIndex: null, isSession };
38
+ }
39
+ toolIndex = Number(rawIndex);
40
+ }
41
+
42
+ if (tokens.length > 0) {
43
+ return { error: 'Too many details arguments.', queryNumber, view, toolIndex, isSession };
44
+ }
45
+
46
+ const result = { error: null, queryNumber, view, toolIndex };
47
+ if (isSession) result.isSession = true;
48
+ return result;
49
+ }
50
+
51
+ export function classifyToolResult(result = {}) {
52
+ const count = result?.count ?? result?.returnedCount;
53
+ if (result?.status === 'error') return { state: 'error', count };
54
+ if (result?.status === 'not_found' || count === 0) return { state: 'not_found', count };
55
+ return { state: 'ok', count };
56
+ }
57
+
58
+ export function summarizeQueryDetails(details = {}) {
59
+ const tools = Array.isArray(details.tools) ? details.tools : [];
60
+ const toolStates = tools.map(tool => classifyToolResult(tool.result));
61
+ return {
62
+ toolCount: tools.length,
63
+ successfulTools: toolStates.filter(item => item.state === 'ok').length,
64
+ failedTools: toolStates.filter(item => item.state === 'error').length,
65
+ emptyTools: toolStates.filter(item => item.state === 'not_found').length,
66
+ toolDurationMs: tools.reduce((total, tool) => total + (Number(tool.duration) || 0), 0),
67
+ thinkingCharacters: String(details.thinking || '').length,
68
+ answerCharacters: String(details.answer || '').length,
69
+ };
70
+ }
71
+
72
+ export function formatDetailDuration(durationMs) {
73
+ const duration = Number(durationMs);
74
+ if (!Number.isFinite(duration) || duration < 0) return 'n/a';
75
+ if (duration < 1000) return `${Math.round(duration)}ms`;
76
+ return `${(duration / 1000).toFixed(duration < 10000 ? 2 : 1)}s`;
77
+ }
78
+
79
+ export function serializeDetailValue(value, maxLength = Infinity) {
80
+ let serialized;
81
+ try {
82
+ serialized = JSON.stringify(value ?? {}, null, 2);
83
+ } catch {
84
+ serialized = String(value ?? '');
85
+ }
86
+ if (!Number.isFinite(maxLength) || serialized.length <= maxLength) {
87
+ return { text: serialized, truncated: false };
88
+ }
89
+ return { text: `${serialized.slice(0, maxLength)}\n… result truncated`, truncated: true };
90
+ }
91
+
92
+ export function copyToClipboard(text) {
93
+ if (typeof text !== 'string') text = String(text ?? '');
94
+ const platform = process.platform;
95
+
96
+ try {
97
+ if (platform === 'win32') {
98
+ const proc = spawnSync('clip', [], { input: text, encoding: 'utf-8', windowsHide: true });
99
+ if (proc.status === 0) return true;
100
+ const ps = spawnSync('powershell.exe', ['-NoProfile', '-Command', '$input | Set-Clipboard'], {
101
+ input: text,
102
+ encoding: 'utf-8',
103
+ windowsHide: true,
104
+ });
105
+ return ps.status === 0;
106
+ }
107
+ if (platform === 'darwin') {
108
+ const proc = spawnSync('pbcopy', [], { input: text, encoding: 'utf-8' });
109
+ return proc.status === 0;
110
+ }
111
+ const tools = [
112
+ { cmd: 'wl-copy', args: [] },
113
+ { cmd: 'xclip', args: ['-selection', 'clipboard'] },
114
+ { cmd: 'xsel', args: ['--clipboard', '--input'] },
115
+ ];
116
+ for (const t of tools) {
117
+ try {
118
+ const proc = spawnSync(t.cmd, t.args, { input: text, encoding: 'utf-8' });
119
+ if (proc.status === 0) return true;
120
+ } catch {}
121
+ }
122
+ } catch {}
123
+ return false;
124
+ }
125
+
126
+ export function getCopyableContent(details, request = {}, session = null) {
127
+ const isSessionReq = request.isSession || (!details && session);
128
+ const view = request.view || 'answer';
129
+
130
+ if (isSessionReq && session) {
131
+ if (view === 'json') {
132
+ return {
133
+ title: `Full Session JSON • ${session.id}`,
134
+ text: typeof session.getTranscriptJson === 'function' ? session.getTranscriptJson() : JSON.stringify(session.toSnapshot?.() || session, null, 2),
135
+ type: 'session_json',
136
+ };
137
+ }
138
+ if (view === 'text' || view === 'raw') {
139
+ return {
140
+ title: `Full Session Text • ${session.id}`,
141
+ text: typeof session.getTranscriptText === 'function' ? session.getTranscriptText() : String(session.messages || ''),
142
+ type: 'session_text',
143
+ };
144
+ }
145
+ return {
146
+ title: `Full Session Markdown • ${session.id}`,
147
+ text: typeof session.getTranscriptMarkdown === 'function' ? session.getTranscriptMarkdown() : String(session.messages || ''),
148
+ type: 'session_markdown',
149
+ };
150
+ }
151
+
152
+ if (!details) return null;
153
+ const queryLabel = `Q${details.number}`;
154
+
155
+ if (view === 'answer') {
156
+ return {
157
+ title: `Answer • ${queryLabel}`,
158
+ text: details.answer || '',
159
+ type: 'answer',
160
+ };
161
+ }
162
+
163
+ if (view === 'thinking') {
164
+ return {
165
+ title: `Thinking Trace • ${queryLabel}`,
166
+ text: details.thinking || '',
167
+ type: 'thinking',
168
+ };
169
+ }
170
+
171
+ if (view === 'tool') {
172
+ const tool = details.tools?.[request.toolIndex - 1];
173
+ if (!tool) return null;
174
+ return {
175
+ title: `Tool #${request.toolIndex} (${tool.name}) • ${queryLabel}`,
176
+ text: JSON.stringify({ name: tool.name, duration: tool.duration, args: tool.args, result: tool.result }, null, 2),
177
+ type: 'tool',
178
+ };
179
+ }
180
+
181
+ if (view === 'tools') {
182
+ const serialized = JSON.stringify(details.tools || [], null, 2);
183
+ return {
184
+ title: `Tool Timeline • ${queryLabel}`,
185
+ text: serialized,
186
+ type: 'tools',
187
+ };
188
+ }
189
+
190
+ if (view === 'json') {
191
+ return {
192
+ title: `Query Diagnostics JSON • ${queryLabel}`,
193
+ text: JSON.stringify(details, null, 2),
194
+ type: 'json',
195
+ };
196
+ }
197
+
198
+ if (view === 'markdown' || view === 'all') {
199
+ const md = [
200
+ `# Question ${details.number}`,
201
+ `**Status**: ${details.status || 'complete'}`,
202
+ `**Runtime**: ${details.provider || 'unknown'} / ${details.model || 'unknown'}`,
203
+ `**Started**: ${Number.isFinite(details.startedAt) ? new Date(details.startedAt).toISOString() : 'n/a'}`,
204
+ `**Duration**: ${formatDetailDuration(details.durationMs)}`,
205
+ '',
206
+ ...(details.prompt ? ['## User Prompt', details.prompt, ''] : []),
207
+ ...(details.thinking ? ['## Thinking Trace', '```', details.thinking, '```', ''] : []),
208
+ ...(details.tools?.length ? ['## Tools Used', '```json', JSON.stringify(details.tools, null, 2), '```', ''] : []),
209
+ '## Answer',
210
+ details.answer || '',
211
+ ].join('\n');
212
+ return {
213
+ title: `Complete Query Markdown • ${queryLabel}`,
214
+ text: md,
215
+ type: 'markdown',
216
+ };
217
+ }
218
+
219
+ const summary = summarizeQueryDetails(details);
220
+ const overviewText = [
221
+ `Query: Q${details.number}`,
222
+ `Status: ${details.status}`,
223
+ `Provider / Model: ${details.provider || 'unknown'} / ${details.model || 'unknown'}`,
224
+ `Duration: ${formatDetailDuration(details.durationMs)}`,
225
+ `Answer (${summary.answerCharacters} chars):\n${details.answer || ''}`,
226
+ details.thinking ? `\nThinking (${summary.thinkingCharacters} chars):\n${details.thinking}` : '',
227
+ ].filter(Boolean).join('\n');
228
+
229
+ return {
230
+ title: `Overview • ${queryLabel}`,
231
+ text: overviewText,
232
+ type: 'overview',
233
+ };
234
+ }