@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 CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@crewx/shared",
3
- "version": "0.0.2",
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 to allow graceful degradation if not installed
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 { randomUUID, createHash } = require('crypto');
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.pragma('journal_mode = WAL');
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
- const spanId = randomUUID();
303
- const attributes = JSON.stringify({
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, parentTaskId, `[skill:${skillName}] ${command}`, now, command, attributes);
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(result || null, completedAt, completedAt, spanId);
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
- 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);
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 = randomUUID();
347
- const attributes = JSON.stringify({
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, `[skill:${skillName}] ${command}`, now, command, attributes);
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(result || null, completedAt, completedAt, spanId);
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
- 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);
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
- const resultStr = result !== undefined ? JSON.stringify(result) : null;
424
- t.ok(resultStr);
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
- });