@crewx/shared 0.0.2 → 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 +205 -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 기록 (항상, 중복 상관없이)
|
|
@@ -299,43 +365,87 @@ function trace(skillName, command, options = {}) {
|
|
|
299
365
|
// crewx skill x로 실행된 경우 → spans 테이블에 child span으로 기록
|
|
300
366
|
const parentTaskId = process.env.CREWX_TASK_ID;
|
|
301
367
|
if (parentTaskId && parentTaskId.trim() !== '') {
|
|
302
|
-
|
|
303
|
-
const
|
|
368
|
+
// Verify FK target exists before INSERT to avoid constraint error
|
|
369
|
+
const taskExists = db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(parentTaskId);
|
|
370
|
+
const safeTaskId = taskExists ? parentTaskId : null;
|
|
371
|
+
|
|
372
|
+
const spanId = generateId('spn');
|
|
373
|
+
const attrs = {
|
|
304
374
|
skill: skillName,
|
|
305
375
|
agent_id: resolvedAgentId,
|
|
306
376
|
tracer_version: '0.3.0'
|
|
307
|
-
}
|
|
377
|
+
};
|
|
378
|
+
if (skillVersion) attrs.skill_version = skillVersion;
|
|
379
|
+
const attributes = JSON.stringify(attrs);
|
|
308
380
|
|
|
309
381
|
db.prepare(`
|
|
310
382
|
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
311
383
|
VALUES (?, ?, ?, 'internal', 'ok', ?, ?, ?)
|
|
312
|
-
`).run(spanId,
|
|
384
|
+
`).run(spanId, safeTaskId, skillName, now, command, attributes);
|
|
313
385
|
|
|
314
386
|
return {
|
|
315
387
|
taskId: parentTaskId,
|
|
316
388
|
spanId,
|
|
317
|
-
ok: (result) => {
|
|
389
|
+
ok: (result, meta) => {
|
|
318
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
|
+
}
|
|
319
405
|
db.prepare(`
|
|
320
406
|
UPDATE spans
|
|
321
407
|
SET status = 'ok',
|
|
322
408
|
output = ?,
|
|
409
|
+
attributes = ?,
|
|
323
410
|
completed_at = ?,
|
|
324
411
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
325
412
|
WHERE id = ?
|
|
326
|
-
`).run(
|
|
413
|
+
`).run(out.text, JSON.stringify(attrs), completedAt, completedAt, spanId);
|
|
327
414
|
db.close();
|
|
328
415
|
},
|
|
329
|
-
fail: (error) => {
|
|
416
|
+
fail: (error, meta) => {
|
|
330
417
|
const completedAt = new Date().toISOString();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
}
|
|
339
449
|
db.close();
|
|
340
450
|
},
|
|
341
451
|
_skipped: false
|
|
@@ -343,44 +453,84 @@ function trace(skillName, command, options = {}) {
|
|
|
343
453
|
}
|
|
344
454
|
|
|
345
455
|
// 직접 호출 (독립 실행) → spans 테이블에 task_id=NULL로 기록
|
|
346
|
-
const spanId =
|
|
347
|
-
const
|
|
456
|
+
const spanId = generateId('spn');
|
|
457
|
+
const attrs = {
|
|
348
458
|
skill: skillName,
|
|
349
459
|
agent_id: resolvedAgentId,
|
|
350
460
|
direct_call: true,
|
|
351
461
|
tracer_version: '0.3.0'
|
|
352
|
-
}
|
|
462
|
+
};
|
|
463
|
+
if (skillVersion) attrs.skill_version = skillVersion;
|
|
464
|
+
const attributes = JSON.stringify(attrs);
|
|
353
465
|
|
|
354
466
|
db.prepare(`
|
|
355
467
|
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
356
468
|
VALUES (?, NULL, ?, 'internal', 'ok', ?, ?, ?)
|
|
357
|
-
`).run(spanId,
|
|
469
|
+
`).run(spanId, skillName, now, command, attributes);
|
|
358
470
|
|
|
359
471
|
return {
|
|
360
472
|
taskId: null,
|
|
361
473
|
spanId,
|
|
362
|
-
ok: (result) => {
|
|
474
|
+
ok: (result, meta) => {
|
|
363
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
|
+
}
|
|
364
490
|
db.prepare(`
|
|
365
491
|
UPDATE spans
|
|
366
492
|
SET status = 'ok',
|
|
367
493
|
output = ?,
|
|
494
|
+
attributes = ?,
|
|
368
495
|
completed_at = ?,
|
|
369
496
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
370
497
|
WHERE id = ?
|
|
371
|
-
`).run(
|
|
498
|
+
`).run(out.text, JSON.stringify(attrs), completedAt, completedAt, spanId);
|
|
372
499
|
db.close();
|
|
373
500
|
},
|
|
374
|
-
fail: (error) => {
|
|
501
|
+
fail: (error, meta) => {
|
|
375
502
|
const completedAt = new Date().toISOString();
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
}
|
|
384
534
|
db.close();
|
|
385
535
|
},
|
|
386
536
|
_skipped: false
|
|
@@ -418,15 +568,33 @@ async function run(skillName, fn, options = {}) {
|
|
|
418
568
|
const command = process.argv.slice(2).join(' ');
|
|
419
569
|
const t = trace(skillName, command, options);
|
|
420
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
|
+
|
|
421
583
|
try {
|
|
422
584
|
const result = await fn();
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
}
|
|
425
592
|
return result;
|
|
426
593
|
} catch (e) {
|
|
594
|
+
process.stdout.write = origWrite;
|
|
427
595
|
t.fail(e.message || String(e));
|
|
428
596
|
throw e;
|
|
429
597
|
}
|
|
430
598
|
}
|
|
431
599
|
|
|
432
|
-
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
|
-
});
|