@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 CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@crewx/shared",
3
- "version": "0.0.3",
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 기록 (항상, 중복 상관없이)
@@ -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 = randomUUID();
307
- const attributes = JSON.stringify({
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, `[skill:${skillName}] ${command}`, now, command, attributes);
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(result || null, completedAt, completedAt, spanId);
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
- db.prepare(`
336
- UPDATE spans
337
- SET status = 'error',
338
- error = ?,
339
- completed_at = ?,
340
- duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
341
- WHERE id = ?
342
- `).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
+ }
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 = randomUUID();
351
- const attributes = JSON.stringify({
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, `[skill:${skillName}] ${command}`, now, command, attributes);
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(result || null, completedAt, completedAt, spanId);
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
- db.prepare(`
381
- UPDATE spans
382
- SET status = 'error',
383
- error = ?,
384
- completed_at = ?,
385
- duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
386
- WHERE id = ?
387
- `).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
+ }
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
- const resultStr = result !== undefined ? JSON.stringify(result) : null;
428
- 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
+ }
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
- });