@crewx/shared 0.0.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/__tests__/skill-tracer.test.ts +37 -0
- package/package.json +6 -0
- package/skill-tracer.js +432 -0
- package/test-tracer.js +54 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
describe('shared/skill-tracer', () => {
|
|
6
|
+
const sharedDir = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
it('should be requireable from shared package', () => {
|
|
9
|
+
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
10
|
+
expect(tracer).toBeDefined();
|
|
11
|
+
expect(typeof tracer.trace).toBe('function');
|
|
12
|
+
expect(typeof tracer.run).toBe('function');
|
|
13
|
+
expect(typeof tracer.getDbPath).toBe('function');
|
|
14
|
+
expect(typeof tracer.getTracesDbPath).toBe('function');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should have package.json with correct name', () => {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(sharedDir, 'package.json'), 'utf-8'));
|
|
19
|
+
expect(pkg.name).toBe('@crewx/shared');
|
|
20
|
+
expect(pkg.main).toBe('skill-tracer.js');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('re-export wrapper at skills/lib should work', () => {
|
|
24
|
+
const wrapperPath = path.resolve(__dirname, '../../../../skills/lib/skill-tracer.js');
|
|
25
|
+
const tracer = require(wrapperPath);
|
|
26
|
+
expect(tracer).toBeDefined();
|
|
27
|
+
expect(typeof tracer.trace).toBe('function');
|
|
28
|
+
expect(typeof tracer.run).toBe('function');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('getDbPath should default to ~/.crewx/crewx.db', () => {
|
|
32
|
+
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
33
|
+
const dbPath = tracer.getDbPath();
|
|
34
|
+
expect(dbPath).toContain('.crewx');
|
|
35
|
+
expect(dbPath).toContain('crewx.db');
|
|
36
|
+
});
|
|
37
|
+
});
|
package/package.json
ADDED
package/skill-tracer.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-tracer.js - 스킬 실행 추적 라이브러리 (PoC)
|
|
3
|
+
*
|
|
4
|
+
* 스킬에서 직접 node skill.js 실행해도 로깅됨
|
|
5
|
+
* - usage.log: 텍스트 파일 로그 (경량)
|
|
6
|
+
* - crewx.db: SQLite DB 로그 (상세)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Lazy-load better-sqlite3 to allow graceful degradation if not installed
|
|
10
|
+
let Database;
|
|
11
|
+
try {
|
|
12
|
+
Database = require('better-sqlite3');
|
|
13
|
+
} catch {
|
|
14
|
+
Database = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const { randomUUID, createHash } = require('crypto');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* crewx.db 경로 찾기
|
|
24
|
+
*/
|
|
25
|
+
function getDbPath() {
|
|
26
|
+
if (process.env.CREWX_TRACES_DB || process.env.CREWX_DB) {
|
|
27
|
+
return process.env.CREWX_TRACES_DB || process.env.CREWX_DB;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return path.join(os.homedir(), '.crewx', 'crewx.db');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Backward-compatible alias
|
|
34
|
+
const getTracesDbPath = getDbPath;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* crewx.db 연결 및 테이블 확인
|
|
38
|
+
* Returns null if better-sqlite3 is unavailable or DB cannot be opened.
|
|
39
|
+
*/
|
|
40
|
+
function getDb() {
|
|
41
|
+
if (!Database) return null;
|
|
42
|
+
|
|
43
|
+
const dbPath = getDbPath();
|
|
44
|
+
const dir = path.dirname(dbPath);
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const db = new Database(dbPath);
|
|
51
|
+
db.pragma('journal_mode = WAL');
|
|
52
|
+
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
agent_id TEXT NOT NULL,
|
|
57
|
+
user_id TEXT,
|
|
58
|
+
prompt TEXT NOT NULL,
|
|
59
|
+
mode TEXT NOT NULL DEFAULT 'execute',
|
|
60
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
61
|
+
result TEXT,
|
|
62
|
+
error TEXT,
|
|
63
|
+
started_at TEXT NOT NULL,
|
|
64
|
+
completed_at TEXT,
|
|
65
|
+
duration_ms INTEGER,
|
|
66
|
+
metadata TEXT,
|
|
67
|
+
project_id TEXT,
|
|
68
|
+
project_name TEXT
|
|
69
|
+
)
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS spans (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
task_id TEXT,
|
|
76
|
+
parent_span_id TEXT,
|
|
77
|
+
name TEXT NOT NULL,
|
|
78
|
+
kind TEXT NOT NULL DEFAULT 'internal',
|
|
79
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
80
|
+
started_at TEXT NOT NULL,
|
|
81
|
+
completed_at TEXT,
|
|
82
|
+
duration_ms INTEGER,
|
|
83
|
+
input TEXT,
|
|
84
|
+
output TEXT,
|
|
85
|
+
error TEXT,
|
|
86
|
+
attributes TEXT,
|
|
87
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
|
|
88
|
+
FOREIGN KEY (parent_span_id) REFERENCES spans(id) ON DELETE SET NULL
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
ensureProjectColumns(db);
|
|
93
|
+
ensureSpansTaskIdNullable(db);
|
|
94
|
+
|
|
95
|
+
return db;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ensureProjectColumns(db) {
|
|
99
|
+
try {
|
|
100
|
+
const columns = db.prepare('PRAGMA table_info(tasks)').all().map((col) => col.name);
|
|
101
|
+
if (!columns.includes('project_id')) {
|
|
102
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN project_id TEXT`);
|
|
103
|
+
}
|
|
104
|
+
if (!columns.includes('project_name')) {
|
|
105
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN project_name TEXT`);
|
|
106
|
+
}
|
|
107
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id)`);
|
|
108
|
+
} catch {
|
|
109
|
+
// Best-effort; ignore failures
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureSpansTaskIdNullable(db) {
|
|
114
|
+
try {
|
|
115
|
+
const columns = db.prepare('PRAGMA table_info(spans)').all();
|
|
116
|
+
const taskIdColumn = columns.find((col) => col.name === 'task_id');
|
|
117
|
+
if (!taskIdColumn || taskIdColumn.notnull === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
122
|
+
db.exec('BEGIN');
|
|
123
|
+
db.exec(`
|
|
124
|
+
CREATE TABLE spans_backup (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
task_id TEXT,
|
|
127
|
+
parent_span_id TEXT,
|
|
128
|
+
name TEXT NOT NULL,
|
|
129
|
+
kind TEXT NOT NULL DEFAULT 'internal',
|
|
130
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
131
|
+
started_at TEXT NOT NULL,
|
|
132
|
+
completed_at TEXT,
|
|
133
|
+
duration_ms INTEGER,
|
|
134
|
+
input TEXT,
|
|
135
|
+
output TEXT,
|
|
136
|
+
error TEXT,
|
|
137
|
+
attributes TEXT,
|
|
138
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
|
|
139
|
+
FOREIGN KEY (parent_span_id) REFERENCES spans_backup(id) ON DELETE SET NULL
|
|
140
|
+
)
|
|
141
|
+
`);
|
|
142
|
+
db.exec(`
|
|
143
|
+
INSERT INTO spans_backup (
|
|
144
|
+
id, task_id, parent_span_id, name, kind, status, started_at,
|
|
145
|
+
completed_at, duration_ms, input, output, error, attributes
|
|
146
|
+
)
|
|
147
|
+
SELECT
|
|
148
|
+
id, task_id, parent_span_id, name, kind, status, started_at,
|
|
149
|
+
completed_at, duration_ms, input, output, error, attributes
|
|
150
|
+
FROM spans
|
|
151
|
+
`);
|
|
152
|
+
db.exec('DROP TABLE spans');
|
|
153
|
+
db.exec('ALTER TABLE spans_backup RENAME TO spans');
|
|
154
|
+
db.exec('COMMIT');
|
|
155
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
156
|
+
} catch {
|
|
157
|
+
try {
|
|
158
|
+
db.exec('ROLLBACK');
|
|
159
|
+
} catch {
|
|
160
|
+
// Best-effort rollback
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
164
|
+
} catch {
|
|
165
|
+
// Best-effort; ignore failures
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveProjectContext() {
|
|
171
|
+
const projectPath = path.resolve(process.cwd());
|
|
172
|
+
return {
|
|
173
|
+
projectId: createHash('sha256').update(normalizeProjectPath(projectPath)).digest('hex'),
|
|
174
|
+
projectName: resolveProjectName(projectPath),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeProjectPath(projectPath) {
|
|
179
|
+
let resolved = path.resolve(projectPath);
|
|
180
|
+
if (process.platform === 'win32') {
|
|
181
|
+
resolved = resolved.replace(/\\/g, '/');
|
|
182
|
+
resolved = resolved.replace(/^([A-Z]):/, (match, drive) => `${drive.toLowerCase()}:`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (resolved.length > 1 && !/^[a-zA-Z]:\/$/.test(resolved)) {
|
|
186
|
+
resolved = resolved.replace(/\/+$/, '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return resolved;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveProjectName(projectPath) {
|
|
193
|
+
const gitConfigPath = path.join(projectPath, '.git', 'config');
|
|
194
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
195
|
+
try {
|
|
196
|
+
const config = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
197
|
+
const match = config.match(/\[remote\s+"[^"]+"\][^\[]*?url\s*=\s*(.+)/m);
|
|
198
|
+
if (match && match[1]) {
|
|
199
|
+
return extractRepoName(match[1].trim());
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// Fall back to basename
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return path.basename(projectPath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractRepoName(remoteUrl) {
|
|
210
|
+
const cleaned = remoteUrl.replace(/\.git$/i, '');
|
|
211
|
+
const parts = cleaned.split(/[/:]/).filter(Boolean);
|
|
212
|
+
return parts[parts.length - 1] || 'unknown';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 로컬 타임스탬프 (usage.log용)
|
|
217
|
+
*/
|
|
218
|
+
function getLocalTimestamp() {
|
|
219
|
+
const now = new Date();
|
|
220
|
+
const y = now.getFullYear();
|
|
221
|
+
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
222
|
+
const d = String(now.getDate()).padStart(2, '0');
|
|
223
|
+
const h = String(now.getHours()).padStart(2, '0');
|
|
224
|
+
const min = String(now.getMinutes()).padStart(2, '0');
|
|
225
|
+
const s = String(now.getSeconds()).padStart(2, '0');
|
|
226
|
+
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* usage.log에 기록
|
|
231
|
+
*/
|
|
232
|
+
function logToUsageFile(logPath, skillName, command) {
|
|
233
|
+
const timestamp = getLocalTimestamp();
|
|
234
|
+
const line = `${timestamp} | [${skillName}] ${command}\n`;
|
|
235
|
+
fs.appendFileSync(logPath, line);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 스킬 실행 추적 시작
|
|
240
|
+
*
|
|
241
|
+
* @param {string} skillName - 스킬 이름 (예: 'memory-v2')
|
|
242
|
+
* @param {string} command - 실행 명령어 (예: 'save agent1 test')
|
|
243
|
+
* @param {object} [options] - 옵션
|
|
244
|
+
* @param {string} [options.agentId] - 에이전트 ID
|
|
245
|
+
* @param {string} [options.usageLog] - usage.log 파일 경로 (지정하면 파일 로그도 남김)
|
|
246
|
+
* @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
|
|
247
|
+
* @returns {{ taskId: string, ok: (result?: string) => void, fail: (error: string) => void }}
|
|
248
|
+
*/
|
|
249
|
+
function trace(skillName, command, options = {}) {
|
|
250
|
+
const {
|
|
251
|
+
agentId,
|
|
252
|
+
usageLog,
|
|
253
|
+
tracesDb = true
|
|
254
|
+
} = typeof options === 'string' ? { agentId: options } : options;
|
|
255
|
+
|
|
256
|
+
// usage.log 기록 (항상, 중복 상관없이)
|
|
257
|
+
if (usageLog) {
|
|
258
|
+
logToUsageFile(usageLog, skillName, command);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// CREWX_NO_TRACE=1: skip trace recording (usage log still recorded above)
|
|
262
|
+
if (process.env.CREWX_NO_TRACE === '1') {
|
|
263
|
+
return {
|
|
264
|
+
taskId: 'no-trace',
|
|
265
|
+
ok: () => {},
|
|
266
|
+
fail: () => {},
|
|
267
|
+
_skipped: true
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// crewx.db 기록 비활성화된 경우
|
|
272
|
+
if (!tracesDb) {
|
|
273
|
+
return {
|
|
274
|
+
taskId: 'no-trace',
|
|
275
|
+
ok: () => {},
|
|
276
|
+
fail: () => {},
|
|
277
|
+
_skipped: true
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let db;
|
|
282
|
+
try {
|
|
283
|
+
db = getDb();
|
|
284
|
+
} catch {
|
|
285
|
+
db = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!db) {
|
|
289
|
+
return {
|
|
290
|
+
taskId: null,
|
|
291
|
+
ok: () => {},
|
|
292
|
+
fail: () => {},
|
|
293
|
+
_skipped: true
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
const resolvedAgentId = agentId || process.env.CREWX_AGENT_ID || 'direct';
|
|
298
|
+
|
|
299
|
+
// crewx skill x로 실행된 경우 → spans 테이블에 child span으로 기록
|
|
300
|
+
const parentTaskId = process.env.CREWX_TASK_ID;
|
|
301
|
+
if (parentTaskId && parentTaskId.trim() !== '') {
|
|
302
|
+
const spanId = randomUUID();
|
|
303
|
+
const attributes = JSON.stringify({
|
|
304
|
+
skill: skillName,
|
|
305
|
+
agent_id: resolvedAgentId,
|
|
306
|
+
tracer_version: '0.3.0'
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
db.prepare(`
|
|
310
|
+
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
311
|
+
VALUES (?, ?, ?, 'internal', 'ok', ?, ?, ?)
|
|
312
|
+
`).run(spanId, parentTaskId, `[skill:${skillName}] ${command}`, now, command, attributes);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
taskId: parentTaskId,
|
|
316
|
+
spanId,
|
|
317
|
+
ok: (result) => {
|
|
318
|
+
const completedAt = new Date().toISOString();
|
|
319
|
+
db.prepare(`
|
|
320
|
+
UPDATE spans
|
|
321
|
+
SET status = 'ok',
|
|
322
|
+
output = ?,
|
|
323
|
+
completed_at = ?,
|
|
324
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
325
|
+
WHERE id = ?
|
|
326
|
+
`).run(result || null, completedAt, completedAt, spanId);
|
|
327
|
+
db.close();
|
|
328
|
+
},
|
|
329
|
+
fail: (error) => {
|
|
330
|
+
const completedAt = new Date().toISOString();
|
|
331
|
+
db.prepare(`
|
|
332
|
+
UPDATE spans
|
|
333
|
+
SET status = 'error',
|
|
334
|
+
error = ?,
|
|
335
|
+
completed_at = ?,
|
|
336
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
337
|
+
WHERE id = ?
|
|
338
|
+
`).run(error, completedAt, completedAt, spanId);
|
|
339
|
+
db.close();
|
|
340
|
+
},
|
|
341
|
+
_skipped: false
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 직접 호출 (독립 실행) → spans 테이블에 task_id=NULL로 기록
|
|
346
|
+
const spanId = randomUUID();
|
|
347
|
+
const attributes = JSON.stringify({
|
|
348
|
+
skill: skillName,
|
|
349
|
+
agent_id: resolvedAgentId,
|
|
350
|
+
direct_call: true,
|
|
351
|
+
tracer_version: '0.3.0'
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
db.prepare(`
|
|
355
|
+
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
356
|
+
VALUES (?, NULL, ?, 'internal', 'ok', ?, ?, ?)
|
|
357
|
+
`).run(spanId, `[skill:${skillName}] ${command}`, now, command, attributes);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
taskId: null,
|
|
361
|
+
spanId,
|
|
362
|
+
ok: (result) => {
|
|
363
|
+
const completedAt = new Date().toISOString();
|
|
364
|
+
db.prepare(`
|
|
365
|
+
UPDATE spans
|
|
366
|
+
SET status = 'ok',
|
|
367
|
+
output = ?,
|
|
368
|
+
completed_at = ?,
|
|
369
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
370
|
+
WHERE id = ?
|
|
371
|
+
`).run(result || null, completedAt, completedAt, spanId);
|
|
372
|
+
db.close();
|
|
373
|
+
},
|
|
374
|
+
fail: (error) => {
|
|
375
|
+
const completedAt = new Date().toISOString();
|
|
376
|
+
db.prepare(`
|
|
377
|
+
UPDATE spans
|
|
378
|
+
SET status = 'error',
|
|
379
|
+
error = ?,
|
|
380
|
+
completed_at = ?,
|
|
381
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
382
|
+
WHERE id = ?
|
|
383
|
+
`).run(error, completedAt, completedAt, spanId);
|
|
384
|
+
db.close();
|
|
385
|
+
},
|
|
386
|
+
_skipped: false
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 스킬 실행 래퍼 (권장)
|
|
392
|
+
* try/catch, trace 시작/종료 자동 처리
|
|
393
|
+
*
|
|
394
|
+
* @param {string} skillName - 스킬 이름
|
|
395
|
+
* @param {() => Promise<any>} fn - 실행할 함수
|
|
396
|
+
* @param {object} [options] - 옵션
|
|
397
|
+
* @param {string} [options.usageLog] - usage.log 파일 경로
|
|
398
|
+
* @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
|
|
399
|
+
* @returns {Promise<any>}
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* // 둘 다 기록
|
|
403
|
+
* run('memory-v2', main, {
|
|
404
|
+
* usageLog: path.join(__dirname, 'usage.log'),
|
|
405
|
+
* tracesDb: true
|
|
406
|
+
* });
|
|
407
|
+
*
|
|
408
|
+
* // usage.log만
|
|
409
|
+
* run('memory-v2', main, {
|
|
410
|
+
* usageLog: path.join(__dirname, 'usage.log'),
|
|
411
|
+
* tracesDb: false
|
|
412
|
+
* });
|
|
413
|
+
*
|
|
414
|
+
* // crewx.db만 (기본)
|
|
415
|
+
* run('memory-v2', main);
|
|
416
|
+
*/
|
|
417
|
+
async function run(skillName, fn, options = {}) {
|
|
418
|
+
const command = process.argv.slice(2).join(' ');
|
|
419
|
+
const t = trace(skillName, command, options);
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const result = await fn();
|
|
423
|
+
const resultStr = result !== undefined ? JSON.stringify(result) : null;
|
|
424
|
+
t.ok(resultStr);
|
|
425
|
+
return result;
|
|
426
|
+
} catch (e) {
|
|
427
|
+
t.fail(e.message || String(e));
|
|
428
|
+
throw e;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
module.exports = { trace, run, getDbPath, getTracesDbPath };
|
package/test-tracer.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-tracer 테스트
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { trace, run, getDbPath } = require('./skill-tracer');
|
|
6
|
+
|
|
7
|
+
console.log('=== skill-tracer PoC Test ===\n');
|
|
8
|
+
console.log('crewx.db path:', getDbPath());
|
|
9
|
+
|
|
10
|
+
// 테스트 1: trace() 수동 사용
|
|
11
|
+
console.log('\n[Test 1] trace() 수동 사용');
|
|
12
|
+
const t = trace('test-skill', 'manual test command', 'test-agent');
|
|
13
|
+
console.log('taskId:', t.taskId);
|
|
14
|
+
console.log('skipped:', t._skipped);
|
|
15
|
+
t.ok('test result');
|
|
16
|
+
console.log('-> 완료\n');
|
|
17
|
+
|
|
18
|
+
// 테스트 2: run() 래퍼 사용 (성공)
|
|
19
|
+
console.log('[Test 2] run() 래퍼 - 성공 케이스');
|
|
20
|
+
run('test-skill', async () => {
|
|
21
|
+
console.log('-> 함수 실행 중...');
|
|
22
|
+
return { status: 'success', data: 123 };
|
|
23
|
+
}).then(() => {
|
|
24
|
+
console.log('-> 완료\n');
|
|
25
|
+
|
|
26
|
+
// 테스트 3: run() 래퍼 사용 (실패)
|
|
27
|
+
console.log('[Test 3] run() 래퍼 - 실패 케이스');
|
|
28
|
+
return run('test-skill', async () => {
|
|
29
|
+
throw new Error('의도적 에러');
|
|
30
|
+
}).catch(e => {
|
|
31
|
+
console.log('-> 에러 캡처됨:', e.message);
|
|
32
|
+
console.log('-> 완료\n');
|
|
33
|
+
});
|
|
34
|
+
}).then(() => {
|
|
35
|
+
// 결과 확인
|
|
36
|
+
console.log('=== crewx.db 확인 ===');
|
|
37
|
+
const Database = require('better-sqlite3');
|
|
38
|
+
const db = new Database(getDbPath());
|
|
39
|
+
const rows = db.prepare(`
|
|
40
|
+
SELECT id, agent_id, prompt, status, duration_ms
|
|
41
|
+
FROM tasks
|
|
42
|
+
WHERE prompt LIKE '%test-skill%'
|
|
43
|
+
ORDER BY started_at DESC
|
|
44
|
+
LIMIT 5
|
|
45
|
+
`).all();
|
|
46
|
+
|
|
47
|
+
console.log('\n최근 test-skill 기록:');
|
|
48
|
+
rows.forEach(r => {
|
|
49
|
+
console.log(` [${r.status}] ${r.prompt.substring(0, 50)}... (${r.duration_ms}ms)`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
db.close();
|
|
53
|
+
console.log('\n=== 테스트 완료 ===');
|
|
54
|
+
});
|