@crewx/shared 0.0.6-rc.61 → 0.0.6-rc.62
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/crewx-api-credential.js +1 -138
- package/package.json +8 -2
- package/skill-tracer.js +10 -480
- package/crewx-api-credential.ts +0 -134
- package/test-tracer.js +0 -54
- package/tests/crewx-api-credential.test.ts +0 -119
- package/tests/skill-tracer.test.ts +0 -224
- package/tsconfig.json +0 -18
package/crewx-api-credential.js
CHANGED
|
@@ -1,138 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.CrewxCredentialError = void 0;
|
|
37
|
-
exports.resolveCrewxApiCredential = resolveCrewxApiCredential;
|
|
38
|
-
exports.authorizationHeader = authorizationHeader;
|
|
39
|
-
const fs = __importStar(require("node:fs"));
|
|
40
|
-
const path = __importStar(require("node:path"));
|
|
41
|
-
const sdk_1 = require("@crewx/sdk");
|
|
42
|
-
const RUNTIME_CREDENTIAL_PURPOSES = new Set([
|
|
43
|
-
'notify',
|
|
44
|
-
'wi',
|
|
45
|
-
'chromex-negotiate',
|
|
46
|
-
'pairing',
|
|
47
|
-
'app-dev',
|
|
48
|
-
]);
|
|
49
|
-
// These are sysexits-style values so shell callers can distinguish the local
|
|
50
|
-
// credential failure before an HTTP request is attempted.
|
|
51
|
-
const EXIT_CODES = {
|
|
52
|
-
MISSING: 78,
|
|
53
|
-
BAD_MODE: 77,
|
|
54
|
-
UNREADABLE: 74,
|
|
55
|
-
};
|
|
56
|
-
const HEX_64 = '[0-9a-f]{64}';
|
|
57
|
-
class CrewxCredentialError extends Error {
|
|
58
|
-
code;
|
|
59
|
-
exitCode;
|
|
60
|
-
constructor(code, message) {
|
|
61
|
-
super(message);
|
|
62
|
-
this.name = 'CrewxCredentialError';
|
|
63
|
-
this.code = code;
|
|
64
|
-
this.exitCode = EXIT_CODES[code];
|
|
65
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
exports.CrewxCredentialError = CrewxCredentialError;
|
|
69
|
-
function isErrnoCode(error, code) {
|
|
70
|
-
return typeof error === 'object'
|
|
71
|
-
&& error !== null
|
|
72
|
-
&& 'code' in error
|
|
73
|
-
&& error.code === code;
|
|
74
|
-
}
|
|
75
|
-
function credentialPath(purpose) {
|
|
76
|
-
return path.join((0, sdk_1.getCrewxHome)(), 'auth', 'runtime', `${purpose}.token`);
|
|
77
|
-
}
|
|
78
|
-
function invalidPurpose(purpose) {
|
|
79
|
-
throw new TypeError(`Unsupported runtime credential purpose: ${String(purpose)}`);
|
|
80
|
-
}
|
|
81
|
-
function missingCredential(purpose, filePath) {
|
|
82
|
-
throw new CrewxCredentialError('MISSING', `Runtime credential '${purpose}' is missing at ${filePath}. `
|
|
83
|
-
+ (purpose === 'app-dev'
|
|
84
|
-
? 'The running CrewX may be too old or not started; start it once to create the credential.'
|
|
85
|
-
: 'Run the CrewX server once to create it or set CREWX_API_TOKEN.'));
|
|
86
|
-
}
|
|
87
|
-
function badMode(purpose, filePath) {
|
|
88
|
-
throw new CrewxCredentialError('BAD_MODE', `Runtime credential '${purpose}' at ${filePath} has insecure permissions; expected mode 0600.`);
|
|
89
|
-
}
|
|
90
|
-
function unreadableCredential(purpose, filePath) {
|
|
91
|
-
throw new CrewxCredentialError('UNREADABLE', `Runtime credential '${purpose}' at ${filePath} is unreadable or invalid. `
|
|
92
|
-
+ 'Check the file contents and permissions.');
|
|
93
|
-
}
|
|
94
|
-
function readRuntimeCredential(purpose) {
|
|
95
|
-
const filePath = credentialPath(purpose);
|
|
96
|
-
let stats;
|
|
97
|
-
try {
|
|
98
|
-
// lstat prevents a symlink from bypassing the mode check on POSIX.
|
|
99
|
-
stats = fs.lstatSync(filePath);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
if (isErrnoCode(error, 'ENOENT'))
|
|
103
|
-
missingCredential(purpose, filePath);
|
|
104
|
-
unreadableCredential(purpose, filePath);
|
|
105
|
-
}
|
|
106
|
-
if (!stats.isFile())
|
|
107
|
-
unreadableCredential(purpose, filePath);
|
|
108
|
-
// Windows does not expose POSIX mode semantics; the server contract skips
|
|
109
|
-
// this check there as well.
|
|
110
|
-
if (process.platform !== 'win32' && (stats.mode & 0o777) !== 0o600) {
|
|
111
|
-
badMode(purpose, filePath);
|
|
112
|
-
}
|
|
113
|
-
let token;
|
|
114
|
-
try {
|
|
115
|
-
token = fs.readFileSync(filePath, 'utf8');
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
if (isErrnoCode(error, 'ENOENT'))
|
|
119
|
-
missingCredential(purpose, filePath);
|
|
120
|
-
unreadableCredential(purpose, filePath);
|
|
121
|
-
}
|
|
122
|
-
const expected = new RegExp(`^crewxrt_${purpose}_${HEX_64}$`);
|
|
123
|
-
if (!expected.test(token))
|
|
124
|
-
unreadableCredential(purpose, filePath);
|
|
125
|
-
return token;
|
|
126
|
-
}
|
|
127
|
-
function resolveCrewxApiCredential(purpose, env = process.env) {
|
|
128
|
-
if (!RUNTIME_CREDENTIAL_PURPOSES.has(purpose))
|
|
129
|
-
invalidPurpose(purpose);
|
|
130
|
-
const envToken = env['CREWX_API_TOKEN'];
|
|
131
|
-
if (typeof envToken === 'string' && envToken.length > 0) {
|
|
132
|
-
return { token: envToken, source: 'env' };
|
|
133
|
-
}
|
|
134
|
-
return { token: readRuntimeCredential(purpose), source: 'file' };
|
|
135
|
-
}
|
|
136
|
-
function authorizationHeader(cred) {
|
|
137
|
-
return { Authorization: `Bearer ${cred.token}` };
|
|
138
|
-
}
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewx/shared",
|
|
3
|
-
"version": "0.0.6-rc.
|
|
3
|
+
"version": "0.0.6-rc.62",
|
|
4
4
|
"main": "skill-tracer.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -28,12 +28,18 @@
|
|
|
28
28
|
},
|
|
29
29
|
"description": "Shared utilities for CrewX built-in packages",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@crewx/sdk": "0.9.0-rc.
|
|
31
|
+
"@crewx/sdk": "0.9.0-rc.147"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20.0.0",
|
|
35
35
|
"typescript": "^5.0.0"
|
|
36
36
|
},
|
|
37
|
+
"files": [
|
|
38
|
+
"skill-tracer.js",
|
|
39
|
+
"skill-tracer.d.ts",
|
|
40
|
+
"crewx-api-credential.js",
|
|
41
|
+
"crewx-api-credential.d.ts"
|
|
42
|
+
],
|
|
37
43
|
"scripts": {
|
|
38
44
|
"build": "tsc -p tsconfig.json",
|
|
39
45
|
"test": "echo no tests"
|
package/skill-tracer.js
CHANGED
|
@@ -1,332 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
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_DB) return process.env.CREWX_DB;
|
|
28
|
-
|
|
29
|
-
return path.join(os.homedir(), '.crewx', 'crewx.db');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Backward-compatible alias
|
|
33
|
-
const getTracesDbPath = getDbPath;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Open an already initialized crewx.db for trace writes.
|
|
37
|
-
* This function never creates a database, directory, or schema.
|
|
38
|
-
* Returns null if better-sqlite3 is unavailable, the database is missing, or
|
|
39
|
-
* the existing file does not contain the tracing tables used by this module.
|
|
40
|
-
*/
|
|
41
|
-
function getDb() {
|
|
42
|
-
if (!Database) return null;
|
|
43
|
-
|
|
44
|
-
const dbPath = getDbPath();
|
|
45
|
-
if (!fs.existsSync(dbPath)) return null;
|
|
46
|
-
|
|
47
|
-
let db;
|
|
48
|
-
try {
|
|
49
|
-
db = new Database(dbPath, { fileMustExist: true });
|
|
50
|
-
db.exec('PRAGMA journal_mode = WAL');
|
|
51
|
-
db.exec('PRAGMA busy_timeout = 5000');
|
|
52
|
-
db.exec('PRAGMA foreign_keys = ON');
|
|
53
|
-
|
|
54
|
-
if (!hasTraceSchema(db)) {
|
|
55
|
-
db.close();
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return db;
|
|
60
|
-
} catch (error) {
|
|
61
|
-
try {
|
|
62
|
-
db?.close();
|
|
63
|
-
} catch {
|
|
64
|
-
// Best-effort cleanup after an open or pragma failure.
|
|
65
|
-
}
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function hasTraceSchema(db) {
|
|
71
|
-
const requiredColumns = {
|
|
72
|
-
tasks: ['id'],
|
|
73
|
-
spans: [
|
|
74
|
-
'id',
|
|
75
|
-
'task_id',
|
|
76
|
-
'name',
|
|
77
|
-
'kind',
|
|
78
|
-
'status',
|
|
79
|
-
'started_at',
|
|
80
|
-
'completed_at',
|
|
81
|
-
'duration_ms',
|
|
82
|
-
'input',
|
|
83
|
-
'output',
|
|
84
|
-
'error',
|
|
85
|
-
'attributes',
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return Object.entries(requiredColumns).every(([tableName, columns]) => {
|
|
90
|
-
const availableColumns = new Set(
|
|
91
|
-
db.prepare(`PRAGMA table_info(${tableName})`).all().map((column) => column.name),
|
|
92
|
-
);
|
|
93
|
-
return columns.every((column) => availableColumns.has(column));
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function resolveProjectContext() {
|
|
98
|
-
const projectPath = path.resolve(process.cwd());
|
|
99
|
-
return {
|
|
100
|
-
projectId: createHash('sha256').update(normalizeProjectPath(projectPath)).digest('hex'),
|
|
101
|
-
projectName: resolveProjectName(projectPath),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function normalizeProjectPath(projectPath) {
|
|
106
|
-
let resolved = path.resolve(projectPath);
|
|
107
|
-
if (process.platform === 'win32') {
|
|
108
|
-
resolved = resolved.replace(/\\/g, '/');
|
|
109
|
-
resolved = resolved.replace(/^([A-Z]):/, (match, drive) => `${drive.toLowerCase()}:`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (resolved.length > 1 && !/^[a-zA-Z]:\/$/.test(resolved)) {
|
|
113
|
-
resolved = resolved.replace(/\/+$/, '');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return resolved;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function resolveProjectName(projectPath) {
|
|
120
|
-
const gitConfigPath = path.join(projectPath, '.git', 'config');
|
|
121
|
-
if (fs.existsSync(gitConfigPath)) {
|
|
122
|
-
try {
|
|
123
|
-
const config = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
124
|
-
const match = config.match(/\[remote\s+"[^"]+"\][^\[]*?url\s*=\s*(.+)/m);
|
|
125
|
-
if (match && match[1]) {
|
|
126
|
-
return extractRepoName(match[1].trim());
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
// Fall back to basename
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return path.basename(projectPath);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function extractRepoName(remoteUrl) {
|
|
137
|
-
const cleaned = remoteUrl.replace(/\.git$/i, '');
|
|
138
|
-
const parts = cleaned.split(/[/:]/).filter(Boolean);
|
|
139
|
-
return parts[parts.length - 1] || 'unknown';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* 로컬 타임스탬프 (usage.log용)
|
|
144
|
-
*/
|
|
145
|
-
function getLocalTimestamp() {
|
|
146
|
-
const now = new Date();
|
|
147
|
-
const y = now.getFullYear();
|
|
148
|
-
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
149
|
-
const d = String(now.getDate()).padStart(2, '0');
|
|
150
|
-
const h = String(now.getHours()).padStart(2, '0');
|
|
151
|
-
const min = String(now.getMinutes()).padStart(2, '0');
|
|
152
|
-
const s = String(now.getSeconds()).padStart(2, '0');
|
|
153
|
-
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* usage.log에 기록
|
|
158
|
-
*/
|
|
159
|
-
function logToUsageFile(logPath, skillName, command) {
|
|
160
|
-
const timestamp = getLocalTimestamp();
|
|
161
|
-
const line = `${timestamp} | [${skillName}] ${command}\n`;
|
|
162
|
-
fs.appendFileSync(logPath, line);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 스킬 실행 추적 시작
|
|
167
|
-
*
|
|
168
|
-
* @param {string} skillName - 스킬 이름 (예: 'memory-v2')
|
|
169
|
-
* @param {string} command - 실행 명령어 (예: 'save agent1 test')
|
|
170
|
-
* @param {object} [options] - 옵션
|
|
171
|
-
* @param {string} [options.agentId] - 에이전트 ID
|
|
172
|
-
* @param {string} [options.usageLog] - usage.log 파일 경로 (지정하면 파일 로그도 남김)
|
|
173
|
-
* @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
|
|
174
|
-
* @returns {{ taskId: string, ok: (result?: string) => void, fail: (error: string) => void }}
|
|
175
|
-
*/
|
|
176
|
-
const _versionCache = new Map();
|
|
177
|
-
|
|
178
|
-
function getOwnSkillVersion(skillDir) {
|
|
179
|
-
const resolved = path.resolve(skillDir);
|
|
180
|
-
if (_versionCache.has(resolved)) return _versionCache.get(resolved);
|
|
181
|
-
|
|
182
|
-
const candidates = [
|
|
183
|
-
path.join(resolved, 'SKILL.md'),
|
|
184
|
-
path.join(resolved, '..', 'SKILL.md'),
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
for (const skillMdPath of candidates) {
|
|
188
|
-
try {
|
|
189
|
-
if (!fs.existsSync(skillMdPath)) continue;
|
|
190
|
-
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
191
|
-
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
192
|
-
if (!fmMatch) continue;
|
|
193
|
-
const fm = fmMatch[1];
|
|
194
|
-
const versionMatch = fm.match(/^version:\s*(.+)$/m);
|
|
195
|
-
if (versionMatch) {
|
|
196
|
-
const v = versionMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
197
|
-
_versionCache.set(resolved, v);
|
|
198
|
-
return v;
|
|
199
|
-
}
|
|
200
|
-
const nestedMatch = fm.match(/^\s+version:\s*(.+)$/m);
|
|
201
|
-
if (nestedMatch) {
|
|
202
|
-
const v = nestedMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
203
|
-
_versionCache.set(resolved, v);
|
|
204
|
-
return v;
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_versionCache.set(resolved, null);
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const MAX_OUTPUT_BYTES = 32 * 1024;
|
|
216
|
-
const HALF_MAX = 16 * 1024;
|
|
217
|
-
|
|
218
|
-
function truncateOutput(text) {
|
|
219
|
-
if (!text) return { text: null, truncated: false, originalBytes: 0 };
|
|
220
|
-
|
|
221
|
-
const buf = Buffer.from(text, 'utf-8');
|
|
222
|
-
const bytes = buf.length;
|
|
223
|
-
|
|
224
|
-
if (bytes <= MAX_OUTPUT_BYTES) {
|
|
225
|
-
return { text, truncated: false, originalBytes: bytes };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const headBuf = buf.subarray(0, HALF_MAX);
|
|
229
|
-
const tailBuf = buf.subarray(bytes - HALF_MAX);
|
|
230
|
-
const omitted = bytes - HALF_MAX * 2;
|
|
231
|
-
|
|
232
|
-
const marker = '\n...[omitted=' + omitted + ' bytes]...\n';
|
|
233
|
-
const result = headBuf.toString('utf-8') + marker + tailBuf.toString('utf-8');
|
|
234
|
-
|
|
235
|
-
return { text: result, truncated: true, originalBytes: bytes };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function trace(skillName, command, options = {}) {
|
|
239
|
-
const {
|
|
240
|
-
agentId,
|
|
241
|
-
usageLog,
|
|
242
|
-
tracesDb = true,
|
|
243
|
-
skillVersion,
|
|
244
|
-
} = typeof options === 'string' ? { agentId: options } : options;
|
|
245
|
-
|
|
246
|
-
// usage.log 기록 (항상, 중복 상관없이)
|
|
247
|
-
if (usageLog) {
|
|
248
|
-
logToUsageFile(usageLog, skillName, command);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// CREWX_NO_TRACE=1: skip trace recording (usage log still recorded above)
|
|
252
|
-
if (process.env.CREWX_NO_TRACE === '1') {
|
|
253
|
-
return {
|
|
254
|
-
taskId: 'no-trace',
|
|
255
|
-
ok: () => {},
|
|
256
|
-
fail: () => {},
|
|
257
|
-
_skipped: true
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// crewx.db 기록 비활성화된 경우
|
|
262
|
-
if (!tracesDb) {
|
|
263
|
-
return {
|
|
264
|
-
taskId: 'no-trace',
|
|
265
|
-
ok: () => {},
|
|
266
|
-
fail: () => {},
|
|
267
|
-
_skipped: true
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
let db;
|
|
272
|
-
try {
|
|
273
|
-
db = getDb();
|
|
274
|
-
} catch {
|
|
275
|
-
db = null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (!db) {
|
|
279
|
-
return {
|
|
280
|
-
taskId: null,
|
|
281
|
-
ok: () => {},
|
|
282
|
-
fail: () => {},
|
|
283
|
-
_skipped: true
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
const now = new Date().toISOString();
|
|
287
|
-
const resolvedAgentId = agentId || process.env.CREWX_AGENT_ID || 'direct';
|
|
288
|
-
|
|
289
|
-
// crewx skill x로 실행된 경우 → spans 테이블에 child span으로 기록
|
|
290
|
-
const parentTaskId = process.env.CREWX_TASK_ID;
|
|
291
|
-
if (parentTaskId && parentTaskId.trim() !== '') {
|
|
292
|
-
// Verify FK target exists before INSERT to avoid constraint error
|
|
293
|
-
const taskExists = db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(parentTaskId);
|
|
294
|
-
const safeTaskId = taskExists ? parentTaskId : null;
|
|
295
|
-
|
|
296
|
-
const spanId = generateId('spn');
|
|
297
|
-
const attrs = {
|
|
298
|
-
skill: skillName,
|
|
299
|
-
agent_id: resolvedAgentId,
|
|
300
|
-
tracer_version: '0.3.0'
|
|
301
|
-
};
|
|
302
|
-
if (skillVersion) attrs.skill_version = skillVersion;
|
|
303
|
-
const attributes = JSON.stringify(attrs);
|
|
304
|
-
|
|
305
|
-
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(`
|
|
306
3
|
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
307
4
|
VALUES (?, ?, ?, 'internal', 'ok', ?, ?, ?)
|
|
308
|
-
`).run(spanId,
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
taskId: parentTaskId,
|
|
312
|
-
spanId,
|
|
313
|
-
ok: (result, meta) => {
|
|
314
|
-
const completedAt = new Date().toISOString();
|
|
315
|
-
const out = truncateOutput(result);
|
|
316
|
-
const attrs = JSON.parse(attributes);
|
|
317
|
-
if (out.truncated) {
|
|
318
|
-
attrs.output_truncated = true;
|
|
319
|
-
attrs.output_bytes_original = out.originalBytes;
|
|
320
|
-
}
|
|
321
|
-
if (meta && meta.stderr) {
|
|
322
|
-
const stderrOut = truncateOutput(meta.stderr);
|
|
323
|
-
attrs.stderr = stderrOut.text;
|
|
324
|
-
if (stderrOut.truncated) {
|
|
325
|
-
attrs.stderr_truncated = true;
|
|
326
|
-
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
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(`
|
|
330
6
|
UPDATE spans
|
|
331
7
|
SET status = 'ok',
|
|
332
8
|
output = ?,
|
|
@@ -334,24 +10,7 @@ function trace(skillName, command, options = {}) {
|
|
|
334
10
|
completed_at = ?,
|
|
335
11
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
336
12
|
WHERE id = ?
|
|
337
|
-
`).run(
|
|
338
|
-
db.close();
|
|
339
|
-
},
|
|
340
|
-
fail: (error, meta) => {
|
|
341
|
-
const completedAt = new Date().toISOString();
|
|
342
|
-
let updateAttrs = null;
|
|
343
|
-
if (meta && meta.stderr) {
|
|
344
|
-
const attrs = JSON.parse(attributes);
|
|
345
|
-
const stderrOut = truncateOutput(meta.stderr);
|
|
346
|
-
attrs.stderr = stderrOut.text;
|
|
347
|
-
if (stderrOut.truncated) {
|
|
348
|
-
attrs.stderr_truncated = true;
|
|
349
|
-
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
350
|
-
}
|
|
351
|
-
updateAttrs = JSON.stringify(attrs);
|
|
352
|
-
}
|
|
353
|
-
if (updateAttrs) {
|
|
354
|
-
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(`
|
|
355
14
|
UPDATE spans
|
|
356
15
|
SET status = 'error',
|
|
357
16
|
error = ?,
|
|
@@ -359,59 +18,17 @@ function trace(skillName, command, options = {}) {
|
|
|
359
18
|
completed_at = ?,
|
|
360
19
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
361
20
|
WHERE id = ?
|
|
362
|
-
`).run(
|
|
363
|
-
} else {
|
|
364
|
-
db.prepare(`
|
|
21
|
+
`).run(d,m,E,E,u)}else{r.prepare(`
|
|
365
22
|
UPDATE spans
|
|
366
23
|
SET status = 'error',
|
|
367
24
|
error = ?,
|
|
368
25
|
completed_at = ?,
|
|
369
26
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
370
27
|
WHERE id = ?
|
|
371
|
-
`).run(
|
|
372
|
-
}
|
|
373
|
-
db.close();
|
|
374
|
-
},
|
|
375
|
-
_skipped: false
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// 직접 호출 (독립 실행) → spans 테이블에 task_id=NULL로 기록
|
|
380
|
-
const spanId = generateId('spn');
|
|
381
|
-
const attrs = {
|
|
382
|
-
skill: skillName,
|
|
383
|
-
agent_id: resolvedAgentId,
|
|
384
|
-
direct_call: true,
|
|
385
|
-
tracer_version: '0.3.0'
|
|
386
|
-
};
|
|
387
|
-
if (skillVersion) attrs.skill_version = skillVersion;
|
|
388
|
-
const attributes = JSON.stringify(attrs);
|
|
389
|
-
|
|
390
|
-
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(`
|
|
391
29
|
INSERT INTO spans (id, task_id, name, kind, status, started_at, input, attributes)
|
|
392
30
|
VALUES (?, NULL, ?, 'internal', 'ok', ?, ?, ?)
|
|
393
|
-
`).run(spanId,
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
taskId: null,
|
|
397
|
-
spanId,
|
|
398
|
-
ok: (result, meta) => {
|
|
399
|
-
const completedAt = new Date().toISOString();
|
|
400
|
-
const out = truncateOutput(result);
|
|
401
|
-
const attrs = JSON.parse(attributes);
|
|
402
|
-
if (out.truncated) {
|
|
403
|
-
attrs.output_truncated = true;
|
|
404
|
-
attrs.output_bytes_original = out.originalBytes;
|
|
405
|
-
}
|
|
406
|
-
if (meta && meta.stderr) {
|
|
407
|
-
const stderrOut = truncateOutput(meta.stderr);
|
|
408
|
-
attrs.stderr = stderrOut.text;
|
|
409
|
-
if (stderrOut.truncated) {
|
|
410
|
-
attrs.stderr_truncated = true;
|
|
411
|
-
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
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(`
|
|
415
32
|
UPDATE spans
|
|
416
33
|
SET status = 'ok',
|
|
417
34
|
output = ?,
|
|
@@ -419,24 +36,7 @@ function trace(skillName, command, options = {}) {
|
|
|
419
36
|
completed_at = ?,
|
|
420
37
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
421
38
|
WHERE id = ?
|
|
422
|
-
`).run(
|
|
423
|
-
db.close();
|
|
424
|
-
},
|
|
425
|
-
fail: (error, meta) => {
|
|
426
|
-
const completedAt = new Date().toISOString();
|
|
427
|
-
let updateAttrs = null;
|
|
428
|
-
if (meta && meta.stderr) {
|
|
429
|
-
const attrs = JSON.parse(attributes);
|
|
430
|
-
const stderrOut = truncateOutput(meta.stderr);
|
|
431
|
-
attrs.stderr = stderrOut.text;
|
|
432
|
-
if (stderrOut.truncated) {
|
|
433
|
-
attrs.stderr_truncated = true;
|
|
434
|
-
attrs.stderr_bytes_original = stderrOut.originalBytes;
|
|
435
|
-
}
|
|
436
|
-
updateAttrs = JSON.stringify(attrs);
|
|
437
|
-
}
|
|
438
|
-
if (updateAttrs) {
|
|
439
|
-
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(`
|
|
440
40
|
UPDATE spans
|
|
441
41
|
SET status = 'error',
|
|
442
42
|
error = ?,
|
|
@@ -444,81 +44,11 @@ function trace(skillName, command, options = {}) {
|
|
|
444
44
|
completed_at = ?,
|
|
445
45
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
446
46
|
WHERE id = ?
|
|
447
|
-
`).run(
|
|
448
|
-
} else {
|
|
449
|
-
db.prepare(`
|
|
47
|
+
`).run(T,g,u,u,I)}else{r.prepare(`
|
|
450
48
|
UPDATE spans
|
|
451
49
|
SET status = 'error',
|
|
452
50
|
error = ?,
|
|
453
51
|
completed_at = ?,
|
|
454
52
|
duration_ms = CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)
|
|
455
53
|
WHERE id = ?
|
|
456
|
-
`).run(
|
|
457
|
-
}
|
|
458
|
-
db.close();
|
|
459
|
-
},
|
|
460
|
-
_skipped: false
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* 스킬 실행 래퍼 (권장)
|
|
466
|
-
* try/catch, trace 시작/종료 자동 처리
|
|
467
|
-
*
|
|
468
|
-
* @param {string} skillName - 스킬 이름
|
|
469
|
-
* @param {() => Promise<any>} fn - 실행할 함수
|
|
470
|
-
* @param {object} [options] - 옵션
|
|
471
|
-
* @param {string} [options.usageLog] - usage.log 파일 경로
|
|
472
|
-
* @param {boolean} [options.tracesDb=true] - crewx.db 기록 여부
|
|
473
|
-
* @returns {Promise<any>}
|
|
474
|
-
*
|
|
475
|
-
* @example
|
|
476
|
-
* // 둘 다 기록
|
|
477
|
-
* run('memory-v2', main, {
|
|
478
|
-
* usageLog: path.join(__dirname, 'usage.log'),
|
|
479
|
-
* tracesDb: true
|
|
480
|
-
* });
|
|
481
|
-
*
|
|
482
|
-
* // usage.log만
|
|
483
|
-
* run('memory-v2', main, {
|
|
484
|
-
* usageLog: path.join(__dirname, 'usage.log'),
|
|
485
|
-
* tracesDb: false
|
|
486
|
-
* });
|
|
487
|
-
*
|
|
488
|
-
* // crewx.db만 (기본)
|
|
489
|
-
* run('memory-v2', main);
|
|
490
|
-
*/
|
|
491
|
-
async function run(skillName, fn, options = {}) {
|
|
492
|
-
const command = process.argv.slice(2).join(' ');
|
|
493
|
-
const t = trace(skillName, command, options);
|
|
494
|
-
|
|
495
|
-
if (!t || t._skipped) {
|
|
496
|
-
return fn();
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const origWrite = process.stdout.write.bind(process.stdout);
|
|
500
|
-
const chunks = [];
|
|
501
|
-
process.stdout.write = function (chunk, encoding, cb) {
|
|
502
|
-
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
503
|
-
if (typeof encoding === 'function') { return origWrite(chunk, encoding); }
|
|
504
|
-
return origWrite(chunk, encoding, cb);
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
try {
|
|
508
|
-
const result = await fn();
|
|
509
|
-
process.stdout.write = origWrite;
|
|
510
|
-
if (result !== undefined) {
|
|
511
|
-
t.ok(JSON.stringify(result));
|
|
512
|
-
} else {
|
|
513
|
-
const output = Buffer.concat(chunks).toString('utf-8');
|
|
514
|
-
t.ok(output || null);
|
|
515
|
-
}
|
|
516
|
-
return result;
|
|
517
|
-
} catch (e) {
|
|
518
|
-
process.stdout.write = origWrite;
|
|
519
|
-
t.fail(e.message || String(e));
|
|
520
|
-
throw e;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
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/crewx-api-credential.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { getCrewxHome } from '@crewx/sdk';
|
|
4
|
-
|
|
5
|
-
export type RuntimeCredentialPurpose = 'notify' | 'wi' | 'chromex-negotiate' | 'pairing' | 'app-dev';
|
|
6
|
-
|
|
7
|
-
export interface ResolvedCredential {
|
|
8
|
-
token: string;
|
|
9
|
-
source: 'env' | 'file';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type CrewxCredentialErrorCode = 'MISSING' | 'BAD_MODE' | 'UNREADABLE';
|
|
13
|
-
|
|
14
|
-
const RUNTIME_CREDENTIAL_PURPOSES = new Set<RuntimeCredentialPurpose>([
|
|
15
|
-
'notify',
|
|
16
|
-
'wi',
|
|
17
|
-
'chromex-negotiate',
|
|
18
|
-
'pairing',
|
|
19
|
-
'app-dev',
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
// These are sysexits-style values so shell callers can distinguish the local
|
|
23
|
-
// credential failure before an HTTP request is attempted.
|
|
24
|
-
const EXIT_CODES: Record<CrewxCredentialErrorCode, number> = {
|
|
25
|
-
MISSING: 78,
|
|
26
|
-
BAD_MODE: 77,
|
|
27
|
-
UNREADABLE: 74,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const HEX_64 = '[0-9a-f]{64}';
|
|
31
|
-
|
|
32
|
-
export class CrewxCredentialError extends Error {
|
|
33
|
-
readonly code: CrewxCredentialErrorCode;
|
|
34
|
-
readonly exitCode: number;
|
|
35
|
-
|
|
36
|
-
constructor(code: CrewxCredentialErrorCode, message: string) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.name = 'CrewxCredentialError';
|
|
39
|
-
this.code = code;
|
|
40
|
-
this.exitCode = EXIT_CODES[code];
|
|
41
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isErrnoCode(error: unknown, code: string): boolean {
|
|
46
|
-
return typeof error === 'object'
|
|
47
|
-
&& error !== null
|
|
48
|
-
&& 'code' in error
|
|
49
|
-
&& (error as { code?: unknown }).code === code;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function credentialPath(purpose: RuntimeCredentialPurpose): string {
|
|
53
|
-
return path.join(getCrewxHome(), 'auth', 'runtime', `${purpose}.token`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function invalidPurpose(purpose: RuntimeCredentialPurpose): never {
|
|
57
|
-
throw new TypeError(`Unsupported runtime credential purpose: ${String(purpose)}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function missingCredential(purpose: RuntimeCredentialPurpose, filePath: string): never {
|
|
61
|
-
throw new CrewxCredentialError(
|
|
62
|
-
'MISSING',
|
|
63
|
-
`Runtime credential '${purpose}' is missing at ${filePath}. `
|
|
64
|
-
+ (purpose === 'app-dev'
|
|
65
|
-
? 'The running CrewX may be too old or not started; start it once to create the credential.'
|
|
66
|
-
: 'Run the CrewX server once to create it or set CREWX_API_TOKEN.'),
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function badMode(purpose: RuntimeCredentialPurpose, filePath: string): never {
|
|
71
|
-
throw new CrewxCredentialError(
|
|
72
|
-
'BAD_MODE',
|
|
73
|
-
`Runtime credential '${purpose}' at ${filePath} has insecure permissions; expected mode 0600.`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function unreadableCredential(purpose: RuntimeCredentialPurpose, filePath: string): never {
|
|
78
|
-
throw new CrewxCredentialError(
|
|
79
|
-
'UNREADABLE',
|
|
80
|
-
`Runtime credential '${purpose}' at ${filePath} is unreadable or invalid. `
|
|
81
|
-
+ 'Check the file contents and permissions.',
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function readRuntimeCredential(purpose: RuntimeCredentialPurpose): string {
|
|
86
|
-
const filePath = credentialPath(purpose);
|
|
87
|
-
let stats: fs.Stats;
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// lstat prevents a symlink from bypassing the mode check on POSIX.
|
|
91
|
-
stats = fs.lstatSync(filePath);
|
|
92
|
-
} catch (error: unknown) {
|
|
93
|
-
if (isErrnoCode(error, 'ENOENT')) missingCredential(purpose, filePath);
|
|
94
|
-
unreadableCredential(purpose, filePath);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!stats!.isFile()) unreadableCredential(purpose, filePath);
|
|
98
|
-
|
|
99
|
-
// Windows does not expose POSIX mode semantics; the server contract skips
|
|
100
|
-
// this check there as well.
|
|
101
|
-
if (process.platform !== 'win32' && (stats!.mode & 0o777) !== 0o600) {
|
|
102
|
-
badMode(purpose, filePath);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let token: string;
|
|
106
|
-
try {
|
|
107
|
-
token = fs.readFileSync(filePath, 'utf8');
|
|
108
|
-
} catch (error: unknown) {
|
|
109
|
-
if (isErrnoCode(error, 'ENOENT')) missingCredential(purpose, filePath);
|
|
110
|
-
unreadableCredential(purpose, filePath);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const expected = new RegExp(`^crewxrt_${purpose}_${HEX_64}$`);
|
|
114
|
-
if (!expected.test(token!)) unreadableCredential(purpose, filePath);
|
|
115
|
-
return token!;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function resolveCrewxApiCredential(
|
|
119
|
-
purpose: RuntimeCredentialPurpose,
|
|
120
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
121
|
-
): ResolvedCredential {
|
|
122
|
-
if (!RUNTIME_CREDENTIAL_PURPOSES.has(purpose)) invalidPurpose(purpose);
|
|
123
|
-
|
|
124
|
-
const envToken = env['CREWX_API_TOKEN'];
|
|
125
|
-
if (typeof envToken === 'string' && envToken.length > 0) {
|
|
126
|
-
return { token: envToken, source: 'env' };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { token: readRuntimeCredential(purpose), source: 'file' };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function authorizationHeader(cred: ResolvedCredential): Record<'Authorization', string> {
|
|
133
|
-
return { Authorization: `Bearer ${cred.token}` };
|
|
134
|
-
}
|
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,119 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import * as fs from 'node:fs';
|
|
5
|
-
import * as os from 'node:os';
|
|
6
|
-
import * as path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
authorizationHeader,
|
|
9
|
-
CrewxCredentialError,
|
|
10
|
-
resolveCrewxApiCredential,
|
|
11
|
-
} from '../crewx-api-credential';
|
|
12
|
-
|
|
13
|
-
const VALID_HEX = 'a'.repeat(64);
|
|
14
|
-
|
|
15
|
-
let tmpHome: string;
|
|
16
|
-
const savedEnv = { ...process.env };
|
|
17
|
-
|
|
18
|
-
function runtimePath(purpose: string): string {
|
|
19
|
-
return path.join(tmpHome, 'auth', 'runtime', `${purpose}.token`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function writeCredential(purpose: string, mode = 0o600, token = `crewxrt_${purpose}_${VALID_HEX}`): void {
|
|
23
|
-
const file = runtimePath(purpose);
|
|
24
|
-
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
25
|
-
fs.writeFileSync(file, token, { encoding: 'utf8', mode });
|
|
26
|
-
fs.chmodSync(file, mode);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'crewx-runtime-credential-'));
|
|
31
|
-
process.env['CREWX_HOME'] = tmpHome;
|
|
32
|
-
delete process.env['CREWX_API_TOKEN'];
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
process.env = { ...savedEnv };
|
|
37
|
-
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('resolveCrewxApiCredential', () => {
|
|
41
|
-
it('resolves a purpose-scoped 0600 runtime credential and builds its Bearer header', () => {
|
|
42
|
-
writeCredential('notify');
|
|
43
|
-
|
|
44
|
-
const credential = resolveCrewxApiCredential('notify');
|
|
45
|
-
|
|
46
|
-
expect(credential).toEqual({ token: `crewxrt_notify_${VALID_HEX}`, source: 'file' });
|
|
47
|
-
expect(authorizationHeader(credential)).toEqual({
|
|
48
|
-
Authorization: `Bearer crewxrt_notify_${VALID_HEX}`,
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('resolves the pairing runtime credential', () => {
|
|
53
|
-
writeCredential('pairing');
|
|
54
|
-
|
|
55
|
-
expect(resolveCrewxApiCredential('pairing')).toEqual({
|
|
56
|
-
token: `crewxrt_pairing_${VALID_HEX}`,
|
|
57
|
-
source: 'file',
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('resolves the app-dev runtime credential', () => {
|
|
62
|
-
writeCredential('app-dev');
|
|
63
|
-
|
|
64
|
-
expect(resolveCrewxApiCredential('app-dev')).toEqual({
|
|
65
|
-
token: `crewxrt_app-dev_${VALID_HEX}`,
|
|
66
|
-
source: 'file',
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('prefers CREWX_API_TOKEN without inspecting the runtime credential file', () => {
|
|
71
|
-
writeCredential('wi', 0o644);
|
|
72
|
-
process.env['CREWX_API_TOKEN'] = 'env-token';
|
|
73
|
-
|
|
74
|
-
expect(resolveCrewxApiCredential('wi')).toEqual({ token: 'env-token', source: 'env' });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('fails with MISSING and a non-zero exit code when the runtime file is absent', () => {
|
|
78
|
-
expect(() => resolveCrewxApiCredential('notify')).toThrow(CrewxCredentialError);
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
resolveCrewxApiCredential('notify');
|
|
82
|
-
throw new Error('expected resolveCrewxApiCredential to throw');
|
|
83
|
-
} catch (error) {
|
|
84
|
-
expect(error).toMatchObject({ code: 'MISSING', exitCode: expect.any(Number) });
|
|
85
|
-
expect((error as Error).message).toContain('notify.token');
|
|
86
|
-
expect((error as CrewxCredentialError).exitCode).toBe(78);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('fails with a distinct BAD_MODE error when the runtime file is not 0600', () => {
|
|
91
|
-
writeCredential('notify', 0o644);
|
|
92
|
-
|
|
93
|
-
expect(() => resolveCrewxApiCredential('notify')).toThrow(CrewxCredentialError);
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
resolveCrewxApiCredential('notify');
|
|
97
|
-
throw new Error('expected resolveCrewxApiCredential to throw');
|
|
98
|
-
} catch (error) {
|
|
99
|
-
expect(error).toMatchObject({ code: 'BAD_MODE', exitCode: expect.any(Number) });
|
|
100
|
-
expect((error as Error).message).toContain('0600');
|
|
101
|
-
expect((error as CrewxCredentialError).exitCode).toBe(77);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('fails with UNREADABLE when the file content does not match the purpose contract', () => {
|
|
106
|
-
writeCredential('chromex-negotiate', 0o600, 'not-a-runtime-token');
|
|
107
|
-
|
|
108
|
-
expect(() => resolveCrewxApiCredential('chromex-negotiate')).toThrow(CrewxCredentialError);
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
resolveCrewxApiCredential('chromex-negotiate');
|
|
112
|
-
throw new Error('expected resolveCrewxApiCredential to throw');
|
|
113
|
-
} catch (error) {
|
|
114
|
-
expect(error).toMatchObject({ code: 'UNREADABLE', exitCode: expect.any(Number) });
|
|
115
|
-
expect((error as CrewxCredentialError).exitCode).toBe(74);
|
|
116
|
-
expect((error as Error).message).not.toContain('not-a-runtime-token');
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,224 +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
|
-
function createTracerDb(dbPath: string): void {
|
|
10
|
-
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
11
|
-
const db = new Database(dbPath);
|
|
12
|
-
db.exec(`
|
|
13
|
-
CREATE TABLE tasks (
|
|
14
|
-
id TEXT PRIMARY KEY,
|
|
15
|
-
agent_id TEXT,
|
|
16
|
-
prompt TEXT,
|
|
17
|
-
status TEXT,
|
|
18
|
-
started_at TEXT
|
|
19
|
-
);
|
|
20
|
-
CREATE TABLE spans (
|
|
21
|
-
id TEXT PRIMARY KEY,
|
|
22
|
-
task_id TEXT,
|
|
23
|
-
parent_span_id TEXT,
|
|
24
|
-
name TEXT NOT NULL,
|
|
25
|
-
kind TEXT NOT NULL DEFAULT 'internal',
|
|
26
|
-
status TEXT NOT NULL DEFAULT 'ok',
|
|
27
|
-
started_at TEXT NOT NULL,
|
|
28
|
-
completed_at TEXT,
|
|
29
|
-
duration_ms INTEGER,
|
|
30
|
-
input TEXT,
|
|
31
|
-
output TEXT,
|
|
32
|
-
error TEXT,
|
|
33
|
-
attributes TEXT
|
|
34
|
-
);
|
|
35
|
-
`);
|
|
36
|
-
db.close();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe('shared/skill-tracer', () => {
|
|
40
|
-
const sharedDir = path.resolve(__dirname, '..');
|
|
41
|
-
|
|
42
|
-
it('should be requireable from shared package', () => {
|
|
43
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
44
|
-
expect(tracer).toBeDefined();
|
|
45
|
-
expect(typeof tracer.trace).toBe('function');
|
|
46
|
-
expect(typeof tracer.run).toBe('function');
|
|
47
|
-
expect(typeof tracer.getDbPath).toBe('function');
|
|
48
|
-
expect(typeof tracer.getTracesDbPath).toBe('function');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should have package.json with correct name', () => {
|
|
52
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(sharedDir, 'package.json'), 'utf-8'));
|
|
53
|
-
expect(pkg.name).toBe('@crewx/shared');
|
|
54
|
-
expect(pkg.main).toBe('skill-tracer.js');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('re-export wrapper at skills/lib should work', () => {
|
|
58
|
-
const wrapperPath = path.resolve(__dirname, '../../../../skills/lib/skill-tracer.js');
|
|
59
|
-
const tracer = require(wrapperPath);
|
|
60
|
-
expect(tracer).toBeDefined();
|
|
61
|
-
expect(typeof tracer.trace).toBe('function');
|
|
62
|
-
expect(typeof tracer.run).toBe('function');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('getDbPath should default to ~/.crewx/crewx.db', () => {
|
|
66
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
67
|
-
const dbPath = tracer.getDbPath();
|
|
68
|
-
expect(dbPath).toContain('.crewx');
|
|
69
|
-
expect(dbPath).toContain('crewx.db');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('uses @crewx/sdk generateId from CJS when direct traces create spans', () => {
|
|
73
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
74
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-tracer-'));
|
|
75
|
-
const dbPath = path.join(tempDir, '.crewx', 'crewx.db');
|
|
76
|
-
const previousDbPath = process.env.CREWX_DB;
|
|
77
|
-
const previousTaskId = process.env.CREWX_TASK_ID;
|
|
78
|
-
|
|
79
|
-
process.env.CREWX_DB = dbPath;
|
|
80
|
-
delete process.env.CREWX_TASK_ID;
|
|
81
|
-
createTracerDb(dbPath);
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const trace = tracer.trace('memory', 'index core_sqa');
|
|
85
|
-
expect(trace._skipped).toBe(false);
|
|
86
|
-
expect(trace.taskId).toBeNull();
|
|
87
|
-
expect(trace.spanId).toMatch(SPAN_ID_PATTERN);
|
|
88
|
-
trace.ok('done');
|
|
89
|
-
} finally {
|
|
90
|
-
if (previousDbPath === undefined) {
|
|
91
|
-
delete process.env.CREWX_DB;
|
|
92
|
-
} else {
|
|
93
|
-
process.env.CREWX_DB = previousDbPath;
|
|
94
|
-
}
|
|
95
|
-
if (previousTaskId === undefined) {
|
|
96
|
-
delete process.env.CREWX_TASK_ID;
|
|
97
|
-
} else {
|
|
98
|
-
process.env.CREWX_TASK_ID = previousTaskId;
|
|
99
|
-
}
|
|
100
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('does not create a missing target database and no-ops without throwing', () => {
|
|
105
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
106
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-tracer-missing-'));
|
|
107
|
-
const dbPath = path.join(tempDir, 'nested', 'crewx.db');
|
|
108
|
-
const previousDbPath = process.env.CREWX_DB;
|
|
109
|
-
const previousTaskId = process.env.CREWX_TASK_ID;
|
|
110
|
-
|
|
111
|
-
process.env.CREWX_DB = dbPath;
|
|
112
|
-
delete process.env.CREWX_TASK_ID;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
expect(() => {
|
|
116
|
-
const trace = tracer.trace('memory', 'index core_sqa');
|
|
117
|
-
expect(trace._skipped).toBe(true);
|
|
118
|
-
trace.ok('ignored');
|
|
119
|
-
}).not.toThrow();
|
|
120
|
-
expect(fs.existsSync(dbPath)).toBe(false);
|
|
121
|
-
expect(fs.existsSync(path.dirname(dbPath))).toBe(false);
|
|
122
|
-
} finally {
|
|
123
|
-
if (previousDbPath === undefined) {
|
|
124
|
-
delete process.env.CREWX_DB;
|
|
125
|
-
} else {
|
|
126
|
-
process.env.CREWX_DB = previousDbPath;
|
|
127
|
-
}
|
|
128
|
-
if (previousTaskId === undefined) {
|
|
129
|
-
delete process.env.CREWX_TASK_ID;
|
|
130
|
-
} else {
|
|
131
|
-
process.env.CREWX_TASK_ID = previousTaskId;
|
|
132
|
-
}
|
|
133
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('shared/skill-tracer output capture', () => {
|
|
139
|
-
const sharedDir = path.resolve(__dirname, '..');
|
|
140
|
-
let tempDir: string;
|
|
141
|
-
let dbPath: string;
|
|
142
|
-
let prevDb: string | undefined;
|
|
143
|
-
let prevTaskId: string | undefined;
|
|
144
|
-
|
|
145
|
-
beforeEach(() => {
|
|
146
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tracer-output-'));
|
|
147
|
-
dbPath = path.join(tempDir, 'crewx.db');
|
|
148
|
-
prevDb = process.env.CREWX_DB;
|
|
149
|
-
prevTaskId = process.env.CREWX_TASK_ID;
|
|
150
|
-
process.env.CREWX_DB = dbPath;
|
|
151
|
-
delete process.env.CREWX_TASK_ID;
|
|
152
|
-
createTracerDb(dbPath);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
afterEach(() => {
|
|
156
|
-
if (prevDb === undefined) delete process.env.CREWX_DB;
|
|
157
|
-
else process.env.CREWX_DB = prevDb;
|
|
158
|
-
if (prevTaskId === undefined) delete process.env.CREWX_TASK_ID;
|
|
159
|
-
else process.env.CREWX_TASK_ID = prevTaskId;
|
|
160
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('ok() captures output and stores in span', () => {
|
|
164
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
165
|
-
const t = tracer.trace('test', 'cmd');
|
|
166
|
-
t.ok('hello output');
|
|
167
|
-
const db = new Database(dbPath);
|
|
168
|
-
const row = db.prepare('SELECT output FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
|
|
169
|
-
db.close();
|
|
170
|
-
expect(row.output).toBe('hello output');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('ok() truncates output > 32KB with head/tail pattern', () => {
|
|
174
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
175
|
-
const bigOutput = 'A'.repeat(40000);
|
|
176
|
-
const t = tracer.trace('test', 'cmd');
|
|
177
|
-
t.ok(bigOutput);
|
|
178
|
-
const db = new Database(dbPath);
|
|
179
|
-
const row = db.prepare('SELECT output, attributes FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
|
|
180
|
-
db.close();
|
|
181
|
-
const attrs = JSON.parse(row.attributes);
|
|
182
|
-
expect(attrs.output_truncated).toBe(true);
|
|
183
|
-
expect(attrs.output_bytes_original).toBe(40000);
|
|
184
|
-
expect(row.output).toContain('[omitted=');
|
|
185
|
-
expect(row.output.length).toBeLessThan(40000);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('ok() captures stderr in attributes', () => {
|
|
189
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
190
|
-
const t = tracer.trace('test', 'cmd');
|
|
191
|
-
t.ok('main output', { stderr: 'some error output' });
|
|
192
|
-
const db = new Database(dbPath);
|
|
193
|
-
const row = db.prepare('SELECT output, attributes FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
|
|
194
|
-
db.close();
|
|
195
|
-
expect(row.output).toBe('main output');
|
|
196
|
-
const attrs = JSON.parse(row.attributes);
|
|
197
|
-
expect(attrs.stderr).toBe('some error output');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('ok() truncates large stderr', () => {
|
|
201
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
202
|
-
const bigStderr = 'E'.repeat(40000);
|
|
203
|
-
const t = tracer.trace('test', 'cmd');
|
|
204
|
-
t.ok('output', { stderr: bigStderr });
|
|
205
|
-
const db = new Database(dbPath);
|
|
206
|
-
const row = db.prepare('SELECT attributes FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
|
|
207
|
-
db.close();
|
|
208
|
-
const attrs = JSON.parse(row.attributes);
|
|
209
|
-
expect(attrs.stderr_truncated).toBe(true);
|
|
210
|
-
expect(attrs.stderr_bytes_original).toBe(40000);
|
|
211
|
-
expect(attrs.stderr).toContain('[omitted=');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('run() captures stdout when fn returns void', async () => {
|
|
215
|
-
const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
|
|
216
|
-
await tracer.run('test-skill', async () => {
|
|
217
|
-
process.stdout.write('captured via run\n');
|
|
218
|
-
});
|
|
219
|
-
const db = new Database(dbPath);
|
|
220
|
-
const row = db.prepare('SELECT output FROM spans ORDER BY started_at DESC LIMIT 1').get() as any;
|
|
221
|
-
db.close();
|
|
222
|
-
expect(row.output).toBe('captured via run\n');
|
|
223
|
-
});
|
|
224
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "CommonJS",
|
|
5
|
-
"moduleResolution": "node",
|
|
6
|
-
"rootDir": ".",
|
|
7
|
-
"outDir": ".",
|
|
8
|
-
"declaration": true,
|
|
9
|
-
"declarationMap": false,
|
|
10
|
-
"sourceMap": false,
|
|
11
|
-
"strict": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["crewx-api-credential.ts"],
|
|
17
|
-
"exclude": ["node_modules", "tests"]
|
|
18
|
-
}
|