@crewx/shared 0.0.3 → 0.0.4
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/package.json +5 -2
- package/skill-tracer.d.ts +19 -0
- package/skill-tracer.js +201 -37
- package/__tests__/skill-tracer.test.ts +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewx/shared",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"main": "skill-tracer.js",
|
|
5
|
-
"description": "Shared utilities for CrewX built-in packages"
|
|
5
|
+
"description": "Shared utilities for CrewX built-in packages",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@crewx/sdk": "*"
|
|
8
|
+
}
|
|
6
9
|
}
|
package/skill-tracer.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
declare const tracer: {
|
|
2
2
|
run: (name: string, fn: () => any, opts?: any) => Promise<any>;
|
|
3
|
+
trace: (
|
|
4
|
+
skillName: string,
|
|
5
|
+
command: string,
|
|
6
|
+
options?: {
|
|
7
|
+
agentId?: string;
|
|
8
|
+
usageLog?: string;
|
|
9
|
+
tracesDb?: boolean;
|
|
10
|
+
skillVersion?: string;
|
|
11
|
+
}
|
|
12
|
+
) => {
|
|
13
|
+
taskId: string | null;
|
|
14
|
+
spanId?: string;
|
|
15
|
+
ok: (result?: string | null, meta?: { stderr?: string }) => void;
|
|
16
|
+
fail: (error: string, meta?: { stderr?: string }) => void;
|
|
17
|
+
_skipped: boolean;
|
|
18
|
+
} | null;
|
|
19
|
+
getDbPath: () => string;
|
|
20
|
+
getTracesDbPath: () => string;
|
|
21
|
+
getOwnSkillVersion: (skillDir: string) => string | null;
|
|
3
22
|
};
|
|
4
23
|
export = tracer;
|
package/skill-tracer.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - crewx.db: SQLite DB 로그 (상세)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
// Lazy-load better-sqlite3
|
|
9
|
+
// Lazy-load better-sqlite3 (native prebuild, no WASM)
|
|
10
10
|
let Database;
|
|
11
11
|
try {
|
|
12
12
|
Database = require('better-sqlite3');
|
|
@@ -17,7 +17,8 @@ try {
|
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const fs = require('fs');
|
|
20
|
-
const {
|
|
20
|
+
const { createHash } = require('crypto');
|
|
21
|
+
const { generateId } = require('@crewx/sdk');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* crewx.db 경로 찾기
|
|
@@ -48,7 +49,9 @@ function getDb() {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const db = new Database(dbPath);
|
|
51
|
-
db.
|
|
52
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
53
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
54
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
52
55
|
|
|
53
56
|
db.exec(`
|
|
54
57
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
@@ -246,11 +249,74 @@ function logToUsageFile(logPath, skillName, command) {
|
|
|
246
249
|
* @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
|
|
247
250
|
* @returns {{ taskId: string, ok: (result?: string) => void, fail: (error: string) => void }}
|
|
248
251
|
*/
|
|
252
|
+
const _versionCache = new Map();
|
|
253
|
+
|
|
254
|
+
function getOwnSkillVersion(skillDir) {
|
|
255
|
+
const resolved = path.resolve(skillDir);
|
|
256
|
+
if (_versionCache.has(resolved)) return _versionCache.get(resolved);
|
|
257
|
+
|
|
258
|
+
const candidates = [
|
|
259
|
+
path.join(resolved, 'SKILL.md'),
|
|
260
|
+
path.join(resolved, '..', 'SKILL.md'),
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
for (const skillMdPath of candidates) {
|
|
264
|
+
try {
|
|
265
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
266
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
267
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
268
|
+
if (!fmMatch) continue;
|
|
269
|
+
const fm = fmMatch[1];
|
|
270
|
+
const versionMatch = fm.match(/^version:\s*(.+)$/m);
|
|
271
|
+
if (versionMatch) {
|
|
272
|
+
const v = versionMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
273
|
+
_versionCache.set(resolved, v);
|
|
274
|
+
return v;
|
|
275
|
+
}
|
|
276
|
+
const nestedMatch = fm.match(/^\s+version:\s*(.+)$/m);
|
|
277
|
+
if (nestedMatch) {
|
|
278
|
+
const v = nestedMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
279
|
+
_versionCache.set(resolved, v);
|
|
280
|
+
return v;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_versionCache.set(resolved, null);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const MAX_OUTPUT_BYTES = 32 * 1024;
|
|
292
|
+
const HALF_MAX = 16 * 1024;
|
|
293
|
+
|
|
294
|
+
function truncateOutput(text) {
|
|
295
|
+
if (!text) return { text: null, truncated: false, originalBytes: 0 };
|
|
296
|
+
|
|
297
|
+
const buf = Buffer.from(text, 'utf-8');
|
|
298
|
+
const bytes = buf.length;
|
|
299
|
+
|
|
300
|
+
if (bytes <= MAX_OUTPUT_BYTES) {
|
|
301
|
+
return { text, truncated: false, originalBytes: bytes };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const headBuf = buf.subarray(0, HALF_MAX);
|
|
305
|
+
const tailBuf = buf.subarray(bytes - HALF_MAX);
|
|
306
|
+
const omitted = bytes - HALF_MAX * 2;
|
|
307
|
+
|
|
308
|
+
const marker = '\n...[omitted=' + omitted + ' bytes]...\n';
|
|
309
|
+
const result = headBuf.toString('utf-8') + marker + tailBuf.toString('utf-8');
|
|
310
|
+
|
|
311
|
+
return { text: result, truncated: true, originalBytes: bytes };
|
|
312
|
+
}
|
|
313
|
+
|
|
249
314
|
function trace(skillName, command, options = {}) {
|
|
250
315
|
const {
|
|
251
316
|
agentId,
|
|
252
317
|
usageLog,
|
|
253
|
-
tracesDb = true
|
|
318
|
+
tracesDb = true,
|
|
319
|
+
skillVersion,
|
|
254
320
|
} = typeof options === 'string' ? { agentId: options } : options;
|
|
255
321
|
|
|
256
322
|
// usage.log 기록 (항상, 중복 상관없이)
|
|
@@ -303,43 +369,83 @@ function trace(skillName, command, options = {}) {
|
|
|
303
369
|
const taskExists = db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(parentTaskId);
|
|
304
370
|
const safeTaskId = taskExists ? parentTaskId : null;
|
|
305
371
|
|
|
306
|
-
const spanId =
|
|
307
|
-
const
|
|
372
|
+
const spanId = generateId('spn');
|
|
373
|
+
const attrs = {
|
|
308
374
|
skill: skillName,
|
|
309
375
|
agent_id: resolvedAgentId,
|
|
310
376
|
tracer_version: '0.3.0'
|
|
311
|
-
}
|
|
377
|
+
};
|
|
378
|
+
if (skillVersion) attrs.skill_version = skillVersion;
|
|
379
|
+
const attributes = JSON.stringify(attrs);
|
|
312
380
|
|
|
313
381
|
db.prepare(`
|
|
314
382
|
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
315
383
|
VALUES (?, ?, ?, 'internal', 'ok', ?, ?, ?)
|
|
316
|
-
`).run(spanId, safeTaskId,
|
|
384
|
+
`).run(spanId, safeTaskId, skillName, now, command, attributes);
|
|
317
385
|
|
|
318
386
|
return {
|
|
319
387
|
taskId: parentTaskId,
|
|
320
388
|
spanId,
|
|
321
|
-
ok: (result) => {
|
|
389
|
+
ok: (result, meta) => {
|
|
322
390
|
const completedAt = new Date().toISOString();
|
|
391
|
+
const out = truncateOutput(result);
|
|
392
|
+
const attrs = JSON.parse(attributes);
|
|
393
|
+
if (out.truncated) {
|
|
394
|
+
attrs.output_truncated = true;
|
|
395
|
+
attrs.output_bytes_original = out.originalBytes;
|
|
396
|
+
}
|
|
397
|
+
if (meta && meta.stderr) {
|
|
398
|
+
const stderrOut = truncateOutput(meta.stderr);
|
|
399
|
+
attrs.stderr = stderrOut.text;
|
|
400
|
+
if (stderrOut.truncated) {
|
|
401
|
+
attrs.stderr_truncated = true;
|
|
402
|
+
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
323
405
|
db.prepare(`
|
|
324
406
|
UPDATE spans
|
|
325
407
|
SET status = 'ok',
|
|
326
408
|
output = ?,
|
|
409
|
+
attributes = ?,
|
|
327
410
|
completed_at = ?,
|
|
328
411
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
329
412
|
WHERE id = ?
|
|
330
|
-
`).run(
|
|
413
|
+
`).run(out.text, JSON.stringify(attrs), completedAt, completedAt, spanId);
|
|
331
414
|
db.close();
|
|
332
415
|
},
|
|
333
|
-
fail: (error) => {
|
|
416
|
+
fail: (error, meta) => {
|
|
334
417
|
const completedAt = new Date().toISOString();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
418
|
+
let updateAttrs = null;
|
|
419
|
+
if (meta && meta.stderr) {
|
|
420
|
+
const attrs = JSON.parse(attributes);
|
|
421
|
+
const stderrOut = truncateOutput(meta.stderr);
|
|
422
|
+
attrs.stderr = stderrOut.text;
|
|
423
|
+
if (stderrOut.truncated) {
|
|
424
|
+
attrs.stderr_truncated = true;
|
|
425
|
+
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
426
|
+
}
|
|
427
|
+
updateAttrs = JSON.stringify(attrs);
|
|
428
|
+
}
|
|
429
|
+
if (updateAttrs) {
|
|
430
|
+
db.prepare(`
|
|
431
|
+
UPDATE spans
|
|
432
|
+
SET status = 'error',
|
|
433
|
+
error = ?,
|
|
434
|
+
attributes = ?,
|
|
435
|
+
completed_at = ?,
|
|
436
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
437
|
+
WHERE id = ?
|
|
438
|
+
`).run(error, updateAttrs, completedAt, completedAt, spanId);
|
|
439
|
+
} else {
|
|
440
|
+
db.prepare(`
|
|
441
|
+
UPDATE spans
|
|
442
|
+
SET status = 'error',
|
|
443
|
+
error = ?,
|
|
444
|
+
completed_at = ?,
|
|
445
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
446
|
+
WHERE id = ?
|
|
447
|
+
`).run(error, completedAt, completedAt, spanId);
|
|
448
|
+
}
|
|
343
449
|
db.close();
|
|
344
450
|
},
|
|
345
451
|
_skipped: false
|
|
@@ -347,44 +453,84 @@ function trace(skillName, command, options = {}) {
|
|
|
347
453
|
}
|
|
348
454
|
|
|
349
455
|
// 직접 호출 (독립 실행) → spans 테이블에 task_id=NULL로 기록
|
|
350
|
-
const spanId =
|
|
351
|
-
const
|
|
456
|
+
const spanId = generateId('spn');
|
|
457
|
+
const attrs = {
|
|
352
458
|
skill: skillName,
|
|
353
459
|
agent_id: resolvedAgentId,
|
|
354
460
|
direct_call: true,
|
|
355
461
|
tracer_version: '0.3.0'
|
|
356
|
-
}
|
|
462
|
+
};
|
|
463
|
+
if (skillVersion) attrs.skill_version = skillVersion;
|
|
464
|
+
const attributes = JSON.stringify(attrs);
|
|
357
465
|
|
|
358
466
|
db.prepare(`
|
|
359
467
|
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
360
468
|
VALUES (?, NULL, ?, 'internal', 'ok', ?, ?, ?)
|
|
361
|
-
`).run(spanId,
|
|
469
|
+
`).run(spanId, skillName, now, command, attributes);
|
|
362
470
|
|
|
363
471
|
return {
|
|
364
472
|
taskId: null,
|
|
365
473
|
spanId,
|
|
366
|
-
ok: (result) => {
|
|
474
|
+
ok: (result, meta) => {
|
|
367
475
|
const completedAt = new Date().toISOString();
|
|
476
|
+
const out = truncateOutput(result);
|
|
477
|
+
const attrs = JSON.parse(attributes);
|
|
478
|
+
if (out.truncated) {
|
|
479
|
+
attrs.output_truncated = true;
|
|
480
|
+
attrs.output_bytes_original = out.originalBytes;
|
|
481
|
+
}
|
|
482
|
+
if (meta && meta.stderr) {
|
|
483
|
+
const stderrOut = truncateOutput(meta.stderr);
|
|
484
|
+
attrs.stderr = stderrOut.text;
|
|
485
|
+
if (stderrOut.truncated) {
|
|
486
|
+
attrs.stderr_truncated = true;
|
|
487
|
+
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
368
490
|
db.prepare(`
|
|
369
491
|
UPDATE spans
|
|
370
492
|
SET status = 'ok',
|
|
371
493
|
output = ?,
|
|
494
|
+
attributes = ?,
|
|
372
495
|
completed_at = ?,
|
|
373
496
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
374
497
|
WHERE id = ?
|
|
375
|
-
`).run(
|
|
498
|
+
`).run(out.text, JSON.stringify(attrs), completedAt, completedAt, spanId);
|
|
376
499
|
db.close();
|
|
377
500
|
},
|
|
378
|
-
fail: (error) => {
|
|
501
|
+
fail: (error, meta) => {
|
|
379
502
|
const completedAt = new Date().toISOString();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
503
|
+
let updateAttrs = null;
|
|
504
|
+
if (meta && meta.stderr) {
|
|
505
|
+
const attrs = JSON.parse(attributes);
|
|
506
|
+
const stderrOut = truncateOutput(meta.stderr);
|
|
507
|
+
attrs.stderr = stderrOut.text;
|
|
508
|
+
if (stderrOut.truncated) {
|
|
509
|
+
attrs.stderr_truncated = true;
|
|
510
|
+
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
511
|
+
}
|
|
512
|
+
updateAttrs = JSON.stringify(attrs);
|
|
513
|
+
}
|
|
514
|
+
if (updateAttrs) {
|
|
515
|
+
db.prepare(`
|
|
516
|
+
UPDATE spans
|
|
517
|
+
SET status = 'error',
|
|
518
|
+
error = ?,
|
|
519
|
+
attributes = ?,
|
|
520
|
+
completed_at = ?,
|
|
521
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
522
|
+
WHERE id = ?
|
|
523
|
+
`).run(error, updateAttrs, completedAt, completedAt, spanId);
|
|
524
|
+
} else {
|
|
525
|
+
db.prepare(`
|
|
526
|
+
UPDATE spans
|
|
527
|
+
SET status = 'error',
|
|
528
|
+
error = ?,
|
|
529
|
+
completed_at = ?,
|
|
530
|
+
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
531
|
+
WHERE id = ?
|
|
532
|
+
`).run(error, completedAt, completedAt, spanId);
|
|
533
|
+
}
|
|
388
534
|
db.close();
|
|
389
535
|
},
|
|
390
536
|
_skipped: false
|
|
@@ -422,15 +568,33 @@ async function run(skillName, fn, options = {}) {
|
|
|
422
568
|
const command = process.argv.slice(2).join(' ');
|
|
423
569
|
const t = trace(skillName, command, options);
|
|
424
570
|
|
|
571
|
+
if (!t || t._skipped) {
|
|
572
|
+
return fn();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
576
|
+
const chunks = [];
|
|
577
|
+
process.stdout.write = function (chunk, encoding, cb) {
|
|
578
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
579
|
+
if (typeof encoding === 'function') { return origWrite(chunk, encoding); }
|
|
580
|
+
return origWrite(chunk, encoding, cb);
|
|
581
|
+
};
|
|
582
|
+
|
|
425
583
|
try {
|
|
426
584
|
const result = await fn();
|
|
427
|
-
|
|
428
|
-
|
|
585
|
+
process.stdout.write = origWrite;
|
|
586
|
+
if (result !== undefined) {
|
|
587
|
+
t.ok(JSON.stringify(result));
|
|
588
|
+
} else {
|
|
589
|
+
const output = Buffer.concat(chunks).toString('utf-8');
|
|
590
|
+
t.ok(output || null);
|
|
591
|
+
}
|
|
429
592
|
return result;
|
|
430
593
|
} catch (e) {
|
|
594
|
+
process.stdout.write = origWrite;
|
|
431
595
|
t.fail(e.message || String(e));
|
|
432
596
|
throw e;
|
|
433
597
|
}
|
|
434
598
|
}
|
|
435
599
|
|
|
436
|
-
module.exports = { trace, run, getDbPath, getTracesDbPath };
|
|
600
|
+
module.exports = { trace, run, getDbPath, getTracesDbPath, getOwnSkillVersion };
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
});
|