@crewx/shared 0.0.6-rc.11 → 0.0.6-rc.110

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.
@@ -0,0 +1,14 @@
1
+ export type RuntimeCredentialPurpose = 'notify' | 'wi' | 'chromex-negotiate' | 'pairing' | 'app-dev';
2
+ export interface ResolvedCredential {
3
+ token: string;
4
+ source: 'env' | 'file';
5
+ }
6
+ type CrewxCredentialErrorCode = 'MISSING' | 'BAD_MODE' | 'UNREADABLE';
7
+ export declare class CrewxCredentialError extends Error {
8
+ readonly code: CrewxCredentialErrorCode;
9
+ readonly exitCode: number;
10
+ constructor(code: CrewxCredentialErrorCode, message: string);
11
+ }
12
+ export declare function resolveCrewxApiCredential(purpose: RuntimeCredentialPurpose, env?: NodeJS.ProcessEnv): ResolvedCredential;
13
+ export declare function authorizationHeader(cred: ResolvedCredential): Record<'Authorization', string>;
14
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";var f=exports&&exports.__createBinding||(Object.create?(function(e,t,n,r){if(r===void 0)r=n;var i=Object.getOwnPropertyDescriptor(t,n);if(!i||("get"in i?!t.__esModule:i.writable||i.configurable)){i={enumerable:true,get:function(){return t[n]}}}Object.defineProperty(e,r,i)}):(function(e,t,n,r){if(r===void 0)r=n;e[r]=t[n]}));var h=exports&&exports.__setModuleDefault||(Object.create?(function(e,t){Object.defineProperty(e,"default",{enumerable:true,value:t})}):function(e,t){e["default"]=t});var u=exports&&exports.__importStar||(function(){var e=function(t){e=Object.getOwnPropertyNames||function(n){var r=[];for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i))r[r.length]=i;return r};return e(t)};return function(t){if(t&&t.__esModule)return t;var n={};if(t!=null){for(var r=e(t),i=0;i<r.length;i++)if(r[i]!=="default")f(n,t,r[i])}h(n,t);return n}})();Object.defineProperty(exports,"__esModule",{value:true});exports.CrewxCredentialError=void 0;exports.resolveCrewxApiCredential=m;exports.authorizationHeader=P;const s=u(require("node:fs"));const _=u(require("node:path"));const E=require("@crewx/sdk");const w=new Set(["notify","wi","chromex-negotiate","pairing","app-dev"]);const C={MISSING:78,BAD_MODE:77,UNREADABLE:74};const v="[0-9a-f]{64}";class o extends Error{code;exitCode;constructor(t,n){super(n);this.name="CrewxCredentialError";this.code=t;this.exitCode=C[t];Object.setPrototypeOf(this,new.target.prototype)}}exports.CrewxCredentialError=o;function l(e,t){return typeof e==="object"&&e!==null&&"code"in e&&e.code===t}function O(e){return _.join((0,E.getCrewxHome)(),"auth","runtime",`${e}.token`)}function g(e){throw new TypeError(`Unsupported runtime credential purpose: ${String(e)}`)}function d(e,t){throw new o("MISSING",`Runtime credential '${e}' is missing at ${t}. `+(e==="app-dev"?"The running CrewX may be too old or not started; start it once to create the credential.":"Run the CrewX server once to create it or set CREWX_API_TOKEN."))}function y(e,t){throw new o("BAD_MODE",`Runtime credential '${e}' at ${t} has insecure permissions; expected mode 0600.`)}function a(e,t){throw new o("UNREADABLE",`Runtime credential '${e}' at ${t} is unreadable or invalid. Check the file contents and permissions.`)}function b(e){const t=O(e);let n;try{n=s.lstatSync(t)}catch(c){if(l(c,"ENOENT"))d(e,t);a(e,t)}if(!n.isFile())a(e,t);if(process.platform!=="win32"&&(n.mode&511)!==384){y(e,t)}let r;try{r=s.readFileSync(t,"utf8")}catch(c){if(l(c,"ENOENT"))d(e,t);a(e,t)}const i=new RegExp(`^crewxrt_${e}_${v}$`);if(!i.test(r))a(e,t);return r}function m(e,t=process.env){if(!w.has(e))g(e);const n=t["CREWX_API_TOKEN"];if(typeof n==="string"&&n.length>0){return{token:n,source:"env"}}return{token:b(e),source:"file"}}function P(e){return{Authorization:`Bearer ${e.token}`}}
package/package.json CHANGED
@@ -1,12 +1,47 @@
1
1
  {
2
2
  "name": "@crewx/shared",
3
- "version": "0.0.6-rc.11",
3
+ "version": "0.0.6-rc.110",
4
4
  "main": "skill-tracer.js",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./skill-tracer.d.ts",
8
+ "require": "./skill-tracer.js",
9
+ "default": "./skill-tracer.js"
10
+ },
11
+ "./skill-tracer": {
12
+ "types": "./skill-tracer.d.ts",
13
+ "require": "./skill-tracer.js",
14
+ "default": "./skill-tracer.js"
15
+ },
16
+ "./crewx-api-credential": {
17
+ "types": "./crewx-api-credential.d.ts",
18
+ "require": "./crewx-api-credential.js",
19
+ "default": "./crewx-api-credential.js"
20
+ }
21
+ },
22
+ "typesVersions": {
23
+ "*": {
24
+ "crewx-api-credential": [
25
+ "crewx-api-credential.d.ts"
26
+ ]
27
+ }
28
+ },
5
29
  "description": "Shared utilities for CrewX built-in packages",
6
30
  "dependencies": {
7
- "@crewx/sdk": "0.8.9-rc.24"
31
+ "@crewx/sdk": "0.9.0-rc.195"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "typescript": "^5.0.0"
8
36
  },
37
+ "files": [
38
+ "skill-tracer.js",
39
+ "skill-tracer.d.ts",
40
+ "crewx-api-credential.js",
41
+ "crewx-api-credential.d.ts"
42
+ ],
9
43
  "scripts": {
44
+ "build": "tsc -p tsconfig.json",
10
45
  "test": "echo no tests"
11
46
  }
12
47
  }
package/skill-tracer.js CHANGED
@@ -1,408 +1,8 @@
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 (native prebuild, no WASM)
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 { createHash } = require('crypto');
21
- const { generateId } = require('@crewx/sdk');
22
-
23
- /**
24
- * crewx.db 경로 찾기
25
- */
26
- function getDbPath() {
27
- if (process.env.CREWX_TRACES_DB || process.env.CREWX_DB) {
28
- return process.env.CREWX_TRACES_DB || process.env.CREWX_DB;
29
- }
30
-
31
- return path.join(os.homedir(), '.crewx', 'crewx.db');
32
- }
33
-
34
- // Backward-compatible alias
35
- const getTracesDbPath = getDbPath;
36
-
37
- /**
38
- * crewx.db 연결 및 테이블 확인
39
- * Returns null if better-sqlite3 is unavailable or DB cannot be opened.
40
- */
41
- function getDb() {
42
- if (!Database) return null;
43
-
44
- const dbPath = getDbPath();
45
- const dir = path.dirname(dbPath);
46
-
47
- if (!fs.existsSync(dir)) {
48
- fs.mkdirSync(dir, { recursive: true });
49
- }
50
-
51
- const db = new Database(dbPath);
52
- db.exec('PRAGMA journal_mode = WAL');
53
- db.exec('PRAGMA busy_timeout = 5000');
54
- db.exec('PRAGMA foreign_keys = ON');
55
-
56
- db.exec(`
57
- CREATE TABLE IF NOT EXISTS tasks (
58
- id TEXT PRIMARY KEY,
59
- agent_id TEXT NOT NULL,
60
- user_id TEXT,
61
- prompt TEXT NOT NULL,
62
- mode TEXT NOT NULL DEFAULT 'execute',
63
- status TEXT NOT NULL DEFAULT 'running',
64
- result TEXT,
65
- error TEXT,
66
- started_at TEXT NOT NULL,
67
- completed_at TEXT,
68
- duration_ms INTEGER,
69
- metadata TEXT,
70
- project_id TEXT,
71
- project_name TEXT
72
- )
73
- `);
74
-
75
- db.exec(`
76
- CREATE TABLE IF NOT EXISTS spans (
77
- id TEXT PRIMARY KEY,
78
- task_id TEXT,
79
- parent_span_id TEXT,
80
- name TEXT NOT NULL,
81
- kind TEXT NOT NULL DEFAULT 'internal',
82
- status TEXT NOT NULL DEFAULT 'ok',
83
- started_at TEXT NOT NULL,
84
- completed_at TEXT,
85
- duration_ms INTEGER,
86
- input TEXT,
87
- output TEXT,
88
- error TEXT,
89
- attributes TEXT,
90
- FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
91
- FOREIGN KEY (parent_span_id) REFERENCES spans(id) ON DELETE SET NULL
92
- )
93
- `);
94
-
95
- ensureProjectColumns(db);
96
- ensureSpansTaskIdNullable(db);
97
-
98
- return db;
99
- }
100
-
101
- function ensureProjectColumns(db) {
102
- try {
103
- const columns = db.prepare('PRAGMA table_info(tasks)').all().map((col) => col.name);
104
- if (!columns.includes('project_id')) {
105
- db.exec(`ALTER TABLE tasks ADD COLUMN project_id TEXT`);
106
- }
107
- if (!columns.includes('project_name')) {
108
- db.exec(`ALTER TABLE tasks ADD COLUMN project_name TEXT`);
109
- }
110
- db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id)`);
111
- } catch {
112
- // Best-effort; ignore failures
113
- }
114
- }
115
-
116
- function ensureSpansTaskIdNullable(db) {
117
- try {
118
- const columns = db.prepare('PRAGMA table_info(spans)').all();
119
- const taskIdColumn = columns.find((col) => col.name === 'task_id');
120
- if (!taskIdColumn || taskIdColumn.notnull === 0) {
121
- return;
122
- }
123
-
124
- db.exec('PRAGMA foreign_keys = OFF');
125
- db.exec('BEGIN');
126
- db.exec(`
127
- CREATE TABLE spans_backup (
128
- id TEXT PRIMARY KEY,
129
- task_id TEXT,
130
- parent_span_id TEXT,
131
- name TEXT NOT NULL,
132
- kind TEXT NOT NULL DEFAULT 'internal',
133
- status TEXT NOT NULL DEFAULT 'ok',
134
- started_at TEXT NOT NULL,
135
- completed_at TEXT,
136
- duration_ms INTEGER,
137
- input TEXT,
138
- output TEXT,
139
- error TEXT,
140
- attributes TEXT,
141
- FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
142
- FOREIGN KEY (parent_span_id) REFERENCES spans_backup(id) ON DELETE SET NULL
143
- )
144
- `);
145
- db.exec(`
146
- INSERT INTO spans_backup (
147
- id, task_id, parent_span_id, name, kind, status, started_at,
148
- completed_at, duration_ms, input, output, error, attributes
149
- )
150
- SELECT
151
- id, task_id, parent_span_id, name, kind, status, started_at,
152
- completed_at, duration_ms, input, output, error, attributes
153
- FROM spans
154
- `);
155
- db.exec('DROP TABLE spans');
156
- db.exec('ALTER TABLE spans_backup RENAME TO spans');
157
- db.exec('COMMIT');
158
- db.exec('PRAGMA foreign_keys = ON');
159
- } catch {
160
- try {
161
- db.exec('ROLLBACK');
162
- } catch {
163
- // Best-effort rollback
164
- }
165
- try {
166
- db.exec('PRAGMA foreign_keys = ON');
167
- } catch {
168
- // Best-effort; ignore failures
169
- }
170
- }
171
- }
172
-
173
- function resolveProjectContext() {
174
- const projectPath = path.resolve(process.cwd());
175
- return {
176
- projectId: createHash('sha256').update(normalizeProjectPath(projectPath)).digest('hex'),
177
- projectName: resolveProjectName(projectPath),
178
- };
179
- }
180
-
181
- function normalizeProjectPath(projectPath) {
182
- let resolved = path.resolve(projectPath);
183
- if (process.platform === 'win32') {
184
- resolved = resolved.replace(/\\/g, '/');
185
- resolved = resolved.replace(/^([A-Z]):/, (match, drive) => `${drive.toLowerCase()}:`);
186
- }
187
-
188
- if (resolved.length > 1 && !/^[a-zA-Z]:\/$/.test(resolved)) {
189
- resolved = resolved.replace(/\/+$/, '');
190
- }
191
-
192
- return resolved;
193
- }
194
-
195
- function resolveProjectName(projectPath) {
196
- const gitConfigPath = path.join(projectPath, '.git', 'config');
197
- if (fs.existsSync(gitConfigPath)) {
198
- try {
199
- const config = fs.readFileSync(gitConfigPath, 'utf-8');
200
- const match = config.match(/\[remote\s+"[^"]+"\][^\[]*?url\s*=\s*(.+)/m);
201
- if (match && match[1]) {
202
- return extractRepoName(match[1].trim());
203
- }
204
- } catch {
205
- // Fall back to basename
206
- }
207
- }
208
-
209
- return path.basename(projectPath);
210
- }
211
-
212
- function extractRepoName(remoteUrl) {
213
- const cleaned = remoteUrl.replace(/\.git$/i, '');
214
- const parts = cleaned.split(/[/:]/).filter(Boolean);
215
- return parts[parts.length - 1] || 'unknown';
216
- }
217
-
218
- /**
219
- * 로컬 타임스탬프 (usage.log용)
220
- */
221
- function getLocalTimestamp() {
222
- const now = new Date();
223
- const y = now.getFullYear();
224
- const m = String(now.getMonth() + 1).padStart(2, '0');
225
- const d = String(now.getDate()).padStart(2, '0');
226
- const h = String(now.getHours()).padStart(2, '0');
227
- const min = String(now.getMinutes()).padStart(2, '0');
228
- const s = String(now.getSeconds()).padStart(2, '0');
229
- return `${y}-${m}-${d} ${h}:${min}:${s}`;
230
- }
231
-
232
- /**
233
- * usage.log에 기록
234
- */
235
- function logToUsageFile(logPath, skillName, command) {
236
- const timestamp = getLocalTimestamp();
237
- const line = `${timestamp} | [${skillName}] ${command}\n`;
238
- fs.appendFileSync(logPath, line);
239
- }
240
-
241
- /**
242
- * 스킬 실행 추적 시작
243
- *
244
- * @param {string} skillName - 스킬 이름 (예: 'memory-v2')
245
- * @param {string} command - 실행 명령어 (예: 'save agent1 test')
246
- * @param {object} [options] - 옵션
247
- * @param {string} [options.agentId] - 에이전트 ID
248
- * @param {string} [options.usageLog] - usage.log 파일 경로 (지정하면 파일 로그도 남김)
249
- * @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
250
- * @returns {{ taskId: string, ok: (result?: string) => void, fail: (error: string) => void }}
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
-
314
- function trace(skillName, command, options = {}) {
315
- const {
316
- agentId,
317
- usageLog,
318
- tracesDb = true,
319
- skillVersion,
320
- } = typeof options === 'string' ? { agentId: options } : options;
321
-
322
- // usage.log 기록 (항상, 중복 상관없이)
323
- if (usageLog) {
324
- logToUsageFile(usageLog, skillName, command);
325
- }
326
-
327
- // CREWX_NO_TRACE=1: skip trace recording (usage log still recorded above)
328
- if (process.env.CREWX_NO_TRACE === '1') {
329
- return {
330
- taskId: 'no-trace',
331
- ok: () => {},
332
- fail: () => {},
333
- _skipped: true
334
- };
335
- }
336
-
337
- // crewx.db 기록 비활성화된 경우
338
- if (!tracesDb) {
339
- return {
340
- taskId: 'no-trace',
341
- ok: () => {},
342
- fail: () => {},
343
- _skipped: true
344
- };
345
- }
346
-
347
- let db;
348
- try {
349
- db = getDb();
350
- } catch {
351
- db = null;
352
- }
353
-
354
- if (!db) {
355
- return {
356
- taskId: null,
357
- ok: () => {},
358
- fail: () => {},
359
- _skipped: true
360
- };
361
- }
362
- const now = new Date().toISOString();
363
- const resolvedAgentId = agentId || process.env.CREWX_AGENT_ID || 'direct';
364
-
365
- // crewx skill x로 실행된 경우 → spans 테이블에 child span으로 기록
366
- const parentTaskId = process.env.CREWX_TASK_ID;
367
- if (parentTaskId && parentTaskId.trim() !== '') {
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 = {
374
- skill: skillName,
375
- agent_id: resolvedAgentId,
376
- tracer_version: '0.3.0'
377
- };
378
- if (skillVersion) attrs.skill_version = skillVersion;
379
- const attributes = JSON.stringify(attrs);
380
-
381
- db.prepare(`
1
+ let v;try{v=require("better-sqlite3")}catch{v=null}const P=require("os");const _=require("path");const h=require("fs");const{createHash:B}=require("crypto");const{generateId:D}=require("@crewx/sdk");function w(){if(process.env.CREWX_DB)return process.env.CREWX_DB;return _.join(P.homedir(),".crewx","crewx.db")}const C=w;function $(){if(!v)return null;const e=w();if(!h.existsSync(e))return null;let t;try{t=new v(e,{fileMustExist:true});t.exec("PRAGMA journal_mode = WAL");t.exec("PRAGMA busy_timeout = 5000");t.exec("PRAGMA foreign_keys = ON");if(!M(t)){t.close();return null}return t}catch(s){try{t?.close()}catch{}throw s}}function M(e){const t={tasks:["id"],spans:["id","task_id","name","kind","status","started_at","completed_at","duration_ms","input","output","error","attributes"]};return Object.entries(t).every(([s,n])=>{const a=new Set(e.prepare(`PRAGMA table_info(${s})`).all().map(o=>o.name));return n.every(o=>a.has(o))})}function X(){const e=_.resolve(process.cwd());return{projectId:B("sha256").update(L(e)).digest("hex"),projectName:W(e)}}function L(e){let t=_.resolve(e);if(process.platform==="win32"){t=t.replace(/\\/g,"/");t=t.replace(/^([A-Z]):/,(s,n)=>`${n.toLowerCase()}:`)}if(t.length>1&&!/^[a-zA-Z]:\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}function W(e){const t=_.join(e,".git","config");if(h.existsSync(t)){try{const s=h.readFileSync(t,"utf-8");const n=s.match(/\[remote\s+"[^"]+"\][^\[]*?url\s*=\s*(.+)/m);if(n&&n[1]){return U(n[1].trim())}}catch{}}return _.basename(e)}function U(e){const t=e.replace(/\.git$/i,"");const s=t.split(/[/:]/).filter(Boolean);return s[s.length-1]||"unknown"}function G(){const e=new Date;const t=e.getFullYear();const s=String(e.getMonth()+1).padStart(2,"0");const n=String(e.getDate()).padStart(2,"0");const a=String(e.getHours()).padStart(2,"0");const o=String(e.getMinutes()).padStart(2,"0");const c=String(e.getSeconds()).padStart(2,"0");return`${t}-${s}-${n} ${a}:${o}:${c}`}function J(e,t,s){const n=G();const a=`${n} | [${t}] ${s}
2
+ `;h.appendFileSync(e,a)}const O=new Map;function H(e){const t=_.resolve(e);if(O.has(t))return O.get(t);const s=[_.join(t,"SKILL.md"),_.join(t,"..","SKILL.md")];for(const n of s){try{if(!h.existsSync(n))continue;const a=h.readFileSync(n,"utf-8");const o=a.match(/^---\r?\n([\s\S]*?)\r?\n---/);if(!o)continue;const c=o[1];const r=c.match(/^version:\s*(.+)$/m);if(r){const f=r[1].trim().replace(/^['"]|['"]$/g,"");O.set(t,f);return f}const l=c.match(/^\s+version:\s*(.+)$/m);if(l){const f=l[1].trim().replace(/^['"]|['"]$/g,"");O.set(t,f);return f}}catch{continue}}O.set(t,null);return null}const q=32*1024;const j=16*1024;function k(e){if(!e)return{text:null,truncated:false,originalBytes:0};const t=Buffer.from(e,"utf-8");const s=t.length;if(s<=q){return{text:e,truncated:false,originalBytes:s}}const n=t.subarray(0,j);const a=t.subarray(s-j);const o=s-j*2;const c="\n...[omitted="+o+" bytes]...\n";const r=n.toString("utf-8")+c+a.toString("utf-8");return{text:r,truncated:true,originalBytes:s}}function x(e,t,s={}){const{agentId:n,usageLog:a,tracesDb:o=true,skillVersion:c}=typeof s==="string"?{agentId:s}:s;if(a){J(a,e,t)}if(process.env.CREWX_NO_TRACE==="1"){return{taskId:"no-trace",ok:()=>{},fail:()=>{},_skipped:true}}if(!o){return{taskId:"no-trace",ok:()=>{},fail:()=>{},_skipped:true}}let r;try{r=$()}catch{r=null}if(!r){return{taskId:null,ok:()=>{},fail:()=>{},_skipped:true}}const l=new Date().toISOString();const f=n||process.env.CREWX_AGENT_ID||"direct";const b=process.env.CREWX_TASK_ID;if(b&&b.trim()!==""){const T=r.prepare("SELECT 1 FROM tasks WHERE id = ?").get(b);const S=T?b:null;const u=D("spn");const g={skill:e,agent_id:f,tracer_version:"0.3.0"};if(c)g.skill_version=c;const i=JSON.stringify(g);r.prepare(`
382
3
  INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
383
4
  VALUES (?, ?, ?, 'internal', 'ok', ?, ?, ?)
384
- `).run(spanId, safeTaskId, skillName, now, command, attributes);
385
-
386
- return {
387
- taskId: parentTaskId,
388
- spanId,
389
- ok: (result, meta) => {
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
- }
405
- db.prepare(`
5
+ `).run(u,S,e,l,t,i);return{taskId:b,spanId:u,ok:(d,y)=>{const E=new Date().toISOString();const m=k(d);const p=JSON.parse(i);if(m.truncated){p.output_truncated=true;p.output_bytes_original=m.originalBytes}if(y&&y.stderr){const A=k(y.stderr);p.stderr=A.text;if(A.truncated){p.stderr_truncated=true;p.stderr_bytes_original=A.originalBytes}}r.prepare(`
406
6
  UPDATE spans
407
7
  SET status = 'ok',
408
8
  output = ?,
@@ -410,24 +10,7 @@ function trace(skillName, command, options = {}) {
410
10
  completed_at = ?,
411
11
  duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
412
12
  WHERE id = ?
413
- `).run(out.text, JSON.stringify(attrs), completedAt, completedAt, spanId);
414
- db.close();
415
- },
416
- fail: (error, meta) => {
417
- const completedAt = new Date().toISOString();
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(`
13
+ `).run(m.text,JSON.stringify(p),E,E,u);r.close()},fail:(d,y)=>{const E=new Date().toISOString();let m=null;if(y&&y.stderr){const p=JSON.parse(i);const A=k(y.stderr);p.stderr=A.text;if(A.truncated){p.stderr_truncated=true;p.stderr_bytes_original=A.originalBytes}m=JSON.stringify(p)}if(m){r.prepare(`
431
14
  UPDATE spans
432
15
  SET status = 'error',
433
16
  error = ?,
@@ -435,59 +18,17 @@ function trace(skillName, command, options = {}) {
435
18
  completed_at = ?,
436
19
  duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
437
20
  WHERE id = ?
438
- `).run(error, updateAttrs, completedAt, completedAt, spanId);
439
- } else {
440
- db.prepare(`
21
+ `).run(d,m,E,E,u)}else{r.prepare(`
441
22
  UPDATE spans
442
23
  SET status = 'error',
443
24
  error = ?,
444
25
  completed_at = ?,
445
26
  duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
446
27
  WHERE id = ?
447
- `).run(error, completedAt, completedAt, spanId);
448
- }
449
- db.close();
450
- },
451
- _skipped: false
452
- };
453
- }
454
-
455
- // 직접 호출 (독립 실행) → spans 테이블에 task_id=NULL로 기록
456
- const spanId = generateId('spn');
457
- const attrs = {
458
- skill: skillName,
459
- agent_id: resolvedAgentId,
460
- direct_call: true,
461
- tracer_version: '0.3.0'
462
- };
463
- if (skillVersion) attrs.skill_version = skillVersion;
464
- const attributes = JSON.stringify(attrs);
465
-
466
- db.prepare(`
28
+ `).run(d,E,E,u)}r.close()},_skipped:false}}const I=D("spn");const N={skill:e,agent_id:f,direct_call:true,tracer_version:"0.3.0"};if(c)N.skill_version=c;const R=JSON.stringify(N);r.prepare(`
467
29
  INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
468
30
  VALUES (?, NULL, ?, 'internal', 'ok', ?, ?, ?)
469
- `).run(spanId, skillName, now, command, attributes);
470
-
471
- return {
472
- taskId: null,
473
- spanId,
474
- ok: (result, meta) => {
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
- }
490
- db.prepare(`
31
+ `).run(I,e,l,t,R);return{taskId:null,spanId:I,ok:(T,S)=>{const u=new Date().toISOString();const g=k(T);const i=JSON.parse(R);if(g.truncated){i.output_truncated=true;i.output_bytes_original=g.originalBytes}if(S&&S.stderr){const d=k(S.stderr);i.stderr=d.text;if(d.truncated){i.stderr_truncated=true;i.stderr_bytes_original=d.originalBytes}}r.prepare(`
491
32
  UPDATE spans
492
33
  SET status = 'ok',
493
34
  output = ?,
@@ -495,24 +36,7 @@ function trace(skillName, command, options = {}) {
495
36
  completed_at = ?,
496
37
  duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
497
38
  WHERE id = ?
498
- `).run(out.text, JSON.stringify(attrs), completedAt, completedAt, spanId);
499
- db.close();
500
- },
501
- fail: (error, meta) => {
502
- const completedAt = new Date().toISOString();
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(`
39
+ `).run(g.text,JSON.stringify(i),u,u,I);r.close()},fail:(T,S)=>{const u=new Date().toISOString();let g=null;if(S&&S.stderr){const i=JSON.parse(R);const d=k(S.stderr);i.stderr=d.text;if(d.truncated){i.stderr_truncated=true;i.stderr_bytes_original=d.originalBytes}g=JSON.stringify(i)}if(g){r.prepare(`
516
40
  UPDATE spans
517
41
  SET status = 'error',
518
42
  error = ?,
@@ -520,81 +44,11 @@ function trace(skillName, command, options = {}) {
520
44
  completed_at = ?,
521
45
  duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
522
46
  WHERE id = ?
523
- `).run(error, updateAttrs, completedAt, completedAt, spanId);
524
- } else {
525
- db.prepare(`
47
+ `).run(T,g,u,u,I)}else{r.prepare(`
526
48
  UPDATE spans
527
49
  SET status = 'error',
528
50
  error = ?,
529
51
  completed_at = ?,
530
52
  duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
531
53
  WHERE id = ?
532
- `).run(error, completedAt, completedAt, spanId);
533
- }
534
- db.close();
535
- },
536
- _skipped: false
537
- };
538
- }
539
-
540
- /**
541
- * 스킬 실행 래퍼 (권장)
542
- * try/catch, trace 시작/종료 자동 처리
543
- *
544
- * @param {string} skillName - 스킬 이름
545
- * @param {() => Promise<any>} fn - 실행할 함수
546
- * @param {object} [options] - 옵션
547
- * @param {string} [options.usageLog] - usage.log 파일 경로
548
- * @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
549
- * @returns {Promise<any>}
550
- *
551
- * @example
552
- * // 둘 다 기록
553
- * run('memory-v2', main, {
554
- * usageLog: path.join(__dirname, 'usage.log'),
555
- * tracesDb: true
556
- * });
557
- *
558
- * // usage.log만
559
- * run('memory-v2', main, {
560
- * usageLog: path.join(__dirname, 'usage.log'),
561
- * tracesDb: false
562
- * });
563
- *
564
- * // crewx.db만 (기본)
565
- * run('memory-v2', main);
566
- */
567
- async function run(skillName, fn, options = {}) {
568
- const command = process.argv.slice(2).join(' ');
569
- const t = trace(skillName, command, options);
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
-
583
- try {
584
- const result = await fn();
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
- }
592
- return result;
593
- } catch (e) {
594
- process.stdout.write = origWrite;
595
- t.fail(e.message || String(e));
596
- throw e;
597
- }
598
- }
599
-
600
- module.exports = { trace, run, getDbPath, getTracesDbPath, getOwnSkillVersion };
54
+ `).run(T,u,u,I)}r.close()},_skipped:false}}async function F(e,t,s={}){const n=process.argv.slice(2).join(" ");const a=x(e,n,s);if(!a||a._skipped){return t()}const o=process.stdout.write.bind(process.stdout);const c=[];process.stdout.write=function(r,l,f){c.push(typeof r==="string"?Buffer.from(r):Buffer.from(r));if(typeof l==="function"){return o(r,l)}return o(r,l,f)};try{const r=await t();process.stdout.write=o;if(r!==void 0){a.ok(JSON.stringify(r))}else{const l=Buffer.concat(c).toString("utf-8");a.ok(l||null)}return r}catch(r){process.stdout.write=o;a.fail(r.message||String(r));throw r}}module.exports={trace:x,run:F,getDbPath:w,getTracesDbPath:C,getOwnSkillVersion:H};
package/test-tracer.js DELETED
@@ -1,54 +0,0 @@
1
- /**
2
- * skill-tracer test
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
- // Test 1: manual trace() use
11
- console.log('\n[Test 1] manual trace() use');
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('-> done\n');
17
-
18
- // Test 2: run() wrapper success
19
- console.log('[Test 2] run() wrapper - success case');
20
- run('test-skill', async () => {
21
- console.log('-> function running...');
22
- return { status: 'success', data: 123 };
23
- }).then(() => {
24
- console.log('-> done\n');
25
-
26
- // Test 3: run() wrapper failure
27
- console.log('[Test 3] run() wrapper - failure case');
28
- return run('test-skill', async () => {
29
- throw new Error('intentional error');
30
- }).catch(e => {
31
- console.log('-> error captured:', e.message);
32
- console.log('-> done\n');
33
- });
34
- }).then(() => {
35
- // Verify results
36
- console.log('=== crewx.db check ===');
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('\nRecent test-skill records:');
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=== test complete ===');
54
- });
@@ -1,159 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import os from 'os';
5
- import Database from 'better-sqlite3';
6
-
7
- const SPAN_ID_PATTERN = /^spn_[A-Za-z0-9]{8}$/;
8
-
9
- describe('shared/skill-tracer', () => {
10
- const sharedDir = path.resolve(__dirname, '..');
11
-
12
- it('should be requireable from shared package', () => {
13
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
14
- expect(tracer).toBeDefined();
15
- expect(typeof tracer.trace).toBe('function');
16
- expect(typeof tracer.run).toBe('function');
17
- expect(typeof tracer.getDbPath).toBe('function');
18
- expect(typeof tracer.getTracesDbPath).toBe('function');
19
- });
20
-
21
- it('should have package.json with correct name', () => {
22
- const pkg = JSON.parse(fs.readFileSync(path.join(sharedDir, 'package.json'), 'utf-8'));
23
- expect(pkg.name).toBe('@crewx/shared');
24
- expect(pkg.main).toBe('skill-tracer.js');
25
- });
26
-
27
- it('re-export wrapper at skills/lib should work', () => {
28
- const wrapperPath = path.resolve(__dirname, '../../../../skills/lib/skill-tracer.js');
29
- const tracer = require(wrapperPath);
30
- expect(tracer).toBeDefined();
31
- expect(typeof tracer.trace).toBe('function');
32
- expect(typeof tracer.run).toBe('function');
33
- });
34
-
35
- it('getDbPath should default to ~/.crewx/crewx.db', () => {
36
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
37
- const dbPath = tracer.getDbPath();
38
- expect(dbPath).toContain('.crewx');
39
- expect(dbPath).toContain('crewx.db');
40
- });
41
-
42
- it('uses @crewx/sdk generateId from CJS when direct traces create spans', () => {
43
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
44
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-tracer-'));
45
- const dbPath = path.join(tempDir, '.crewx', 'crewx.db');
46
- const previousDbPath = process.env.CREWX_DB;
47
- const previousTaskId = process.env.CREWX_TASK_ID;
48
-
49
- process.env.CREWX_DB = dbPath;
50
- delete process.env.CREWX_TASK_ID;
51
-
52
- try {
53
- const trace = tracer.trace('memory', 'index core_sqa');
54
- expect(trace._skipped).toBe(false);
55
- expect(trace.taskId).toBeNull();
56
- expect(trace.spanId).toMatch(SPAN_ID_PATTERN);
57
- trace.ok('done');
58
- } finally {
59
- if (previousDbPath === undefined) {
60
- delete process.env.CREWX_DB;
61
- } else {
62
- process.env.CREWX_DB = previousDbPath;
63
- }
64
- if (previousTaskId === undefined) {
65
- delete process.env.CREWX_TASK_ID;
66
- } else {
67
- process.env.CREWX_TASK_ID = previousTaskId;
68
- }
69
- fs.rmSync(tempDir, { recursive: true, force: true });
70
- }
71
- });
72
- });
73
-
74
- describe('shared/skill-tracer output capture', () => {
75
- const sharedDir = path.resolve(__dirname, '..');
76
- let tempDir: string;
77
- let dbPath: string;
78
- let prevDb: string | undefined;
79
- let prevTaskId: string | undefined;
80
-
81
- beforeEach(() => {
82
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tracer-output-'));
83
- dbPath = path.join(tempDir, 'crewx.db');
84
- prevDb = process.env.CREWX_DB;
85
- prevTaskId = process.env.CREWX_TASK_ID;
86
- process.env.CREWX_DB = dbPath;
87
- delete process.env.CREWX_TASK_ID;
88
- });
89
-
90
- afterEach(() => {
91
- if (prevDb === undefined) delete process.env.CREWX_DB;
92
- else process.env.CREWX_DB = prevDb;
93
- if (prevTaskId === undefined) delete process.env.CREWX_TASK_ID;
94
- else process.env.CREWX_TASK_ID = prevTaskId;
95
- fs.rmSync(tempDir, { recursive: true, force: true });
96
- });
97
-
98
- it('ok() captures output and stores in span', () => {
99
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
100
- const t = tracer.trace('test', 'cmd');
101
- t.ok('hello output');
102
- const db = new Database(dbPath);
103
- const row = db.prepare('SELECT output FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
104
- db.close();
105
- expect(row.output).toBe('hello output');
106
- });
107
-
108
- it('ok() truncates output > 32KB with head/tail pattern', () => {
109
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
110
- const bigOutput = 'A'.repeat(40000);
111
- const t = tracer.trace('test', 'cmd');
112
- t.ok(bigOutput);
113
- const db = new Database(dbPath);
114
- const row = db.prepare('SELECT output, attributes FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
115
- db.close();
116
- const attrs = JSON.parse(row.attributes);
117
- expect(attrs.output_truncated).toBe(true);
118
- expect(attrs.output_bytes_original).toBe(40000);
119
- expect(row.output).toContain('[omitted=');
120
- expect(row.output.length).toBeLessThan(40000);
121
- });
122
-
123
- it('ok() captures stderr in attributes', () => {
124
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
125
- const t = tracer.trace('test', 'cmd');
126
- t.ok('main output', { stderr: 'some error output' });
127
- const db = new Database(dbPath);
128
- const row = db.prepare('SELECT output, attributes FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
129
- db.close();
130
- expect(row.output).toBe('main output');
131
- const attrs = JSON.parse(row.attributes);
132
- expect(attrs.stderr).toBe('some error output');
133
- });
134
-
135
- it('ok() truncates large stderr', () => {
136
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
137
- const bigStderr = 'E'.repeat(40000);
138
- const t = tracer.trace('test', 'cmd');
139
- t.ok('output', { stderr: bigStderr });
140
- const db = new Database(dbPath);
141
- const row = db.prepare('SELECT attributes FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
142
- db.close();
143
- const attrs = JSON.parse(row.attributes);
144
- expect(attrs.stderr_truncated).toBe(true);
145
- expect(attrs.stderr_bytes_original).toBe(40000);
146
- expect(attrs.stderr).toContain('[omitted=');
147
- });
148
-
149
- it('run() captures stdout when fn returns void', async () => {
150
- const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
151
- await tracer.run('test-skill', async () => {
152
- process.stdout.write('captured via run\n');
153
- });
154
- const db = new Database(dbPath);
155
- const row = db.prepare('SELECT output FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
156
- db.close();
157
- expect(row.output).toBe('captured via run\n');
158
- });
159
- });