crewx 0.9.0-rc.63 → 0.9.0-rc.64
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/dist-server/common/cache-db.js +213 -0
- package/dist-server/common/decorator/cached.decorator.js +35 -0
- package/dist-server/domain/auth/session-store.js +90 -14
- package/dist-server/domain/auth/sessions-db.js +82 -0
- package/dist-server/domain/usage/usage.service.js +81 -141
- package/package.json +13 -13
- package/packages/cli/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.kvGet = kvGet;
|
|
4
|
+
exports.kvSet = kvSet;
|
|
5
|
+
exports.kvDel = kvDel;
|
|
6
|
+
exports.kvHas = kvHas;
|
|
7
|
+
exports.close = close;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const sdk_1 = require("@crewx/sdk");
|
|
11
|
+
const MEMORY_MISS = Symbol('cache-miss');
|
|
12
|
+
const memory = new Map();
|
|
13
|
+
let database;
|
|
14
|
+
let databasePath;
|
|
15
|
+
function loadDatabaseConstructor() {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
+
return require('better-sqlite3');
|
|
18
|
+
}
|
|
19
|
+
function secureDatabaseFiles(filePath) {
|
|
20
|
+
for (const path of [filePath, `${filePath}-wal`, `${filePath}-shm`]) {
|
|
21
|
+
try {
|
|
22
|
+
(0, node_fs_1.chmodSync)(path, 0o600);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// SQLite may not have created a sidecar file yet.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function removeDatabaseFiles(filePath) {
|
|
30
|
+
for (const path of [filePath, `${filePath}-wal`, `${filePath}-shm`]) {
|
|
31
|
+
try {
|
|
32
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// The file may not exist or may already be gone.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function initializeDatabase(filePath) {
|
|
40
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(filePath, '..'), { recursive: true, mode: 0o700 });
|
|
41
|
+
const db = new (loadDatabaseConstructor())(filePath);
|
|
42
|
+
try {
|
|
43
|
+
db.exec(`
|
|
44
|
+
PRAGMA journal_mode = WAL;
|
|
45
|
+
PRAGMA busy_timeout = 5000;
|
|
46
|
+
PRAGMA synchronous = NORMAL;
|
|
47
|
+
PRAGMA user_version = 1;
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
50
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
51
|
+
value TEXT NOT NULL,
|
|
52
|
+
expires_at INTEGER NOT NULL,
|
|
53
|
+
CHECK (length(key) BETWEEN 1 AND 1024),
|
|
54
|
+
CHECK (instr(key, char(0)) = 0),
|
|
55
|
+
CHECK (expires_at >= 0)
|
|
56
|
+
) WITHOUT ROWID;
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_kv_expires_at
|
|
59
|
+
ON kv (expires_at);
|
|
60
|
+
`);
|
|
61
|
+
db.prepare('DELETE FROM kv WHERE expires_at <= ?').run(Date.now());
|
|
62
|
+
secureDatabaseFiles(filePath);
|
|
63
|
+
return db;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
try {
|
|
67
|
+
db.close();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Ignore close errors while recovering a broken cache database.
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function openDatabase(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
return initializeDatabase(filePath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
removeDatabaseFiles(filePath);
|
|
81
|
+
try {
|
|
82
|
+
return initializeDatabase(filePath);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function getDatabase() {
|
|
90
|
+
const filePath = (0, node_path_1.join)((0, sdk_1.getCrewxHome)(), 'cache.db');
|
|
91
|
+
if (database && databasePath !== filePath) {
|
|
92
|
+
close();
|
|
93
|
+
}
|
|
94
|
+
if (!database) {
|
|
95
|
+
databasePath = filePath;
|
|
96
|
+
database = openDatabase(filePath);
|
|
97
|
+
}
|
|
98
|
+
return database;
|
|
99
|
+
}
|
|
100
|
+
function deletePersistent(key) {
|
|
101
|
+
const db = getDatabase();
|
|
102
|
+
if (!db)
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
db.prepare('DELETE FROM kv WHERE key = ?').run(key);
|
|
106
|
+
secureDatabaseFiles(databasePath ?? '');
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Cache file writes are best effort.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function getMemoryEntry(key, now) {
|
|
113
|
+
const entry = memory.get(key);
|
|
114
|
+
if (!entry)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (entry.expiresAt <= now) {
|
|
117
|
+
memory.delete(key);
|
|
118
|
+
deletePersistent(key);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return entry;
|
|
122
|
+
}
|
|
123
|
+
function readPersistent(key, now) {
|
|
124
|
+
const db = getDatabase();
|
|
125
|
+
if (!db)
|
|
126
|
+
return MEMORY_MISS;
|
|
127
|
+
let row;
|
|
128
|
+
try {
|
|
129
|
+
row = db.prepare('SELECT value, expires_at FROM kv WHERE key = ?').get(key);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return MEMORY_MISS;
|
|
133
|
+
}
|
|
134
|
+
if (!row)
|
|
135
|
+
return MEMORY_MISS;
|
|
136
|
+
if (row.expires_at <= now) {
|
|
137
|
+
deletePersistent(key);
|
|
138
|
+
return MEMORY_MISS;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(row.value);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
deletePersistent(key);
|
|
145
|
+
return MEMORY_MISS;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function kvGet(key) {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const memoryEntry = getMemoryEntry(key, now);
|
|
151
|
+
if (memoryEntry)
|
|
152
|
+
return memoryEntry.value;
|
|
153
|
+
const value = readPersistent(key, now);
|
|
154
|
+
if (value === MEMORY_MISS)
|
|
155
|
+
return undefined;
|
|
156
|
+
const row = getDatabase()?.prepare('SELECT expires_at FROM kv WHERE key = ?').get(key);
|
|
157
|
+
if (row)
|
|
158
|
+
memory.set(key, { value, expiresAt: row.expires_at });
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
function kvSet(key, value, ttlMs) {
|
|
162
|
+
const expiresAt = Date.now() + ttlMs;
|
|
163
|
+
memory.set(key, { value, expiresAt });
|
|
164
|
+
let serialized;
|
|
165
|
+
try {
|
|
166
|
+
serialized = JSON.stringify(value);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (serialized === undefined)
|
|
172
|
+
return;
|
|
173
|
+
const db = getDatabase();
|
|
174
|
+
if (!db)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
db.prepare(`
|
|
178
|
+
INSERT INTO kv (key, value, expires_at) VALUES (?, ?, ?)
|
|
179
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at
|
|
180
|
+
`).run(key, serialized, expiresAt);
|
|
181
|
+
secureDatabaseFiles(databasePath ?? '');
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// The in-memory value remains available when the cache file is unavailable.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function kvDel(key) {
|
|
188
|
+
memory.delete(key);
|
|
189
|
+
deletePersistent(key);
|
|
190
|
+
}
|
|
191
|
+
function kvHas(key) {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (getMemoryEntry(key, now))
|
|
194
|
+
return true;
|
|
195
|
+
const value = readPersistent(key, now);
|
|
196
|
+
if (value === MEMORY_MISS)
|
|
197
|
+
return false;
|
|
198
|
+
const row = getDatabase()?.prepare('SELECT expires_at FROM kv WHERE key = ?').get(key);
|
|
199
|
+
if (row)
|
|
200
|
+
memory.set(key, { value, expiresAt: row.expires_at });
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
function close() {
|
|
204
|
+
memory.clear();
|
|
205
|
+
try {
|
|
206
|
+
database?.close();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Ignore close errors during shutdown and test cleanup.
|
|
210
|
+
}
|
|
211
|
+
database = undefined;
|
|
212
|
+
databasePath = undefined;
|
|
213
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Cached = Cached;
|
|
4
|
+
const cache_db_js_1 = require("../cache-db.js");
|
|
5
|
+
function isPromiseLike(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';
|
|
7
|
+
}
|
|
8
|
+
function Cached(options) {
|
|
9
|
+
return (_target, _propertyKey, descriptor) => {
|
|
10
|
+
if (!descriptor || typeof descriptor.value !== 'function') {
|
|
11
|
+
throw new TypeError('@Cached can only decorate methods');
|
|
12
|
+
}
|
|
13
|
+
const original = descriptor.value;
|
|
14
|
+
const returnsPromise = original.constructor.name === 'AsyncFunction';
|
|
15
|
+
const wrapped = function (...args) {
|
|
16
|
+
const cacheKey = options.key.replace(/\{(\d+)\}/g, (_match, index) => {
|
|
17
|
+
const argument = args[Number(index)];
|
|
18
|
+
return argument === undefined || argument === null ? '' : String(argument);
|
|
19
|
+
});
|
|
20
|
+
const cached = (0, cache_db_js_1.kvGet)(cacheKey);
|
|
21
|
+
if (cached !== undefined)
|
|
22
|
+
return returnsPromise ? Promise.resolve(cached) : cached;
|
|
23
|
+
const result = original.apply(this, args);
|
|
24
|
+
if (isPromiseLike(result)) {
|
|
25
|
+
return Promise.resolve(result).then((value) => {
|
|
26
|
+
(0, cache_db_js_1.kvSet)(cacheKey, value, options.ttlMs);
|
|
27
|
+
return value;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
(0, cache_db_js_1.kvSet)(cacheKey, result, options.ttlMs);
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
descriptor.value = wrapped;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,26 +1,102 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionStore = void 0;
|
|
3
4
|
exports.createSession = createSession;
|
|
4
5
|
exports.validateSession = validateSession;
|
|
5
6
|
exports.destroySession = destroySession;
|
|
6
|
-
|
|
7
|
-
const
|
|
7
|
+
exports.closeSessionStore = closeSessionStore;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const crewx_home_js_1 = require("../../shared/crewx-home.js");
|
|
11
|
+
const sessions_db_js_1 = require("./sessions-db.js");
|
|
8
12
|
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days (matches cookie maxAge)
|
|
13
|
+
function hashSessionId(sessionId) {
|
|
14
|
+
return (0, node_crypto_1.createHash)('sha256').update(sessionId).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
function defaultSessionsPath() {
|
|
17
|
+
return (0, node_path_1.join)((0, crewx_home_js_1.getCrewxHome)(), 'sessions.db');
|
|
18
|
+
}
|
|
19
|
+
/** Owns one sessions.db handle and its in-memory first-level session cache. */
|
|
20
|
+
class SessionStore {
|
|
21
|
+
database;
|
|
22
|
+
sessions = new Map();
|
|
23
|
+
dbPath;
|
|
24
|
+
constructor(dbPath) {
|
|
25
|
+
this.dbPath = dbPath;
|
|
26
|
+
}
|
|
27
|
+
createSession() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const expiresAt = now + SESSION_MAX_AGE_MS;
|
|
30
|
+
const rawSessionId = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
31
|
+
const hashedSessionId = hashSessionId(rawSessionId);
|
|
32
|
+
// The database write must complete before the raw cookie value is returned.
|
|
33
|
+
this.databaseHandle()
|
|
34
|
+
.prepare('INSERT INTO web_sessions (id, created_at, expires_at) VALUES (?, ?, ?)')
|
|
35
|
+
.run(hashedSessionId, now, expiresAt);
|
|
36
|
+
this.sessions.set(rawSessionId, { expiresAt });
|
|
37
|
+
return rawSessionId;
|
|
38
|
+
}
|
|
39
|
+
validateSession(rawSessionId) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const memorySession = this.sessions.get(rawSessionId);
|
|
42
|
+
if (memorySession) {
|
|
43
|
+
if (memorySession.expiresAt > now)
|
|
44
|
+
return true;
|
|
45
|
+
this.sessions.delete(rawSessionId);
|
|
46
|
+
this.deleteByHash(hashSessionId(rawSessionId));
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const hashedSessionId = hashSessionId(rawSessionId);
|
|
50
|
+
const row = this.databaseHandle()
|
|
51
|
+
.prepare('SELECT id, created_at, expires_at FROM web_sessions WHERE id = ?')
|
|
52
|
+
.get(hashedSessionId);
|
|
53
|
+
if (!row)
|
|
54
|
+
return false;
|
|
55
|
+
if (row.expires_at <= now) {
|
|
56
|
+
this.deleteByHash(hashedSessionId);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
this.sessions.set(rawSessionId, { expiresAt: row.expires_at });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
destroySession(rawSessionId) {
|
|
63
|
+
this.sessions.delete(rawSessionId);
|
|
64
|
+
this.deleteByHash(hashSessionId(rawSessionId));
|
|
65
|
+
}
|
|
66
|
+
/** Close the handle and clear the memory cache, simulating a process restart. */
|
|
67
|
+
close() {
|
|
68
|
+
this.sessions.clear();
|
|
69
|
+
if (!this.database)
|
|
70
|
+
return;
|
|
71
|
+
this.database.close();
|
|
72
|
+
this.database = undefined;
|
|
73
|
+
}
|
|
74
|
+
databaseHandle() {
|
|
75
|
+
if (!this.database) {
|
|
76
|
+
this.database = (0, sessions_db_js_1.openSessionsDatabase)(this.dbPath ?? defaultSessionsPath());
|
|
77
|
+
this.database.prepare('DELETE FROM web_sessions WHERE expires_at <= ?').run(Date.now());
|
|
78
|
+
}
|
|
79
|
+
return this.database;
|
|
80
|
+
}
|
|
81
|
+
deleteByHash(hashedSessionId) {
|
|
82
|
+
this.databaseHandle().prepare('DELETE FROM web_sessions WHERE id = ?').run(hashedSessionId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.SessionStore = SessionStore;
|
|
86
|
+
let defaultStore;
|
|
87
|
+
function getDefaultStore() {
|
|
88
|
+
return (defaultStore ??= new SessionStore());
|
|
89
|
+
}
|
|
9
90
|
function createSession() {
|
|
10
|
-
|
|
11
|
-
sessions.set(sessionId, { createdAt: Date.now() });
|
|
12
|
-
return sessionId;
|
|
91
|
+
return getDefaultStore().createSession();
|
|
13
92
|
}
|
|
14
93
|
function validateSession(sessionId) {
|
|
15
|
-
|
|
16
|
-
if (!session)
|
|
17
|
-
return false;
|
|
18
|
-
if (Date.now() - session.createdAt > SESSION_MAX_AGE_MS) {
|
|
19
|
-
sessions.delete(sessionId);
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
return true;
|
|
94
|
+
return getDefaultStore().validateSession(sessionId);
|
|
23
95
|
}
|
|
24
96
|
function destroySession(sessionId) {
|
|
25
|
-
|
|
97
|
+
getDefaultStore().destroySession(sessionId);
|
|
98
|
+
}
|
|
99
|
+
/** Close the module singleton so the next operation reopens sessions.db. */
|
|
100
|
+
function closeSessionStore() {
|
|
101
|
+
defaultStore?.close();
|
|
26
102
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.openSessionsDatabase = openSessionsDatabase;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
10
|
+
const DATABASE_FILE_MODE = 0o600;
|
|
11
|
+
const DATABASE_DIRECTORY_MODE = 0o700;
|
|
12
|
+
const WEB_SESSIONS_SCHEMA = `
|
|
13
|
+
CREATE TABLE IF NOT EXISTS web_sessions (
|
|
14
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
15
|
+
created_at INTEGER NOT NULL,
|
|
16
|
+
expires_at INTEGER NOT NULL,
|
|
17
|
+
CHECK (
|
|
18
|
+
length(id) = 64
|
|
19
|
+
AND id = lower(id)
|
|
20
|
+
AND id NOT GLOB '*[^0-9a-f]*'
|
|
21
|
+
),
|
|
22
|
+
CHECK (created_at >= 0),
|
|
23
|
+
CHECK (expires_at > created_at)
|
|
24
|
+
) WITHOUT ROWID;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_web_sessions_expires_at
|
|
27
|
+
ON web_sessions (expires_at);
|
|
28
|
+
`;
|
|
29
|
+
function chmodIfPresent(filePath) {
|
|
30
|
+
if ((0, node_fs_1.existsSync)(filePath))
|
|
31
|
+
(0, node_fs_1.chmodSync)(filePath, DATABASE_FILE_MODE);
|
|
32
|
+
}
|
|
33
|
+
function setFilePermissions(dbPath) {
|
|
34
|
+
chmodIfPresent(dbPath);
|
|
35
|
+
chmodIfPresent(`${dbPath}-wal`);
|
|
36
|
+
chmodIfPresent(`${dbPath}-shm`);
|
|
37
|
+
}
|
|
38
|
+
function isCorruptionError(error) {
|
|
39
|
+
if (!(error instanceof Error))
|
|
40
|
+
return false;
|
|
41
|
+
return /not a database|database disk image is malformed|file is encrypted|malformed/i.test(error.message);
|
|
42
|
+
}
|
|
43
|
+
function removeDatabaseFiles(dbPath) {
|
|
44
|
+
for (const filePath of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
45
|
+
try {
|
|
46
|
+
(0, node_fs_1.unlinkSync)(filePath);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== 'ENOENT')
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function initializeDatabase(dbPath) {
|
|
55
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dbPath), { recursive: true, mode: DATABASE_DIRECTORY_MODE });
|
|
56
|
+
const database = new better_sqlite3_1.default(dbPath);
|
|
57
|
+
try {
|
|
58
|
+
database.pragma('journal_mode = WAL');
|
|
59
|
+
database.pragma('busy_timeout = 5000');
|
|
60
|
+
database.pragma('synchronous = FULL');
|
|
61
|
+
database.pragma('user_version = 1');
|
|
62
|
+
database.exec(WEB_SESSIONS_SCHEMA);
|
|
63
|
+
setFilePermissions(dbPath);
|
|
64
|
+
return database;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
database.close();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Open the dedicated web-session database with its durability and permission policy. */
|
|
72
|
+
function openSessionsDatabase(dbPath) {
|
|
73
|
+
try {
|
|
74
|
+
return initializeDatabase(dbPath);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!isCorruptionError(error))
|
|
78
|
+
throw error;
|
|
79
|
+
removeDatabaseFiles(dbPath);
|
|
80
|
+
return initializeDatabase(dbPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -59,15 +59,13 @@ const project_repository_js_1 = require("../../repository/project.repository.js"
|
|
|
59
59
|
const usage_limit_snapshot_repository_js_1 = require("../../repository/usage-limit-snapshot.repository.js");
|
|
60
60
|
const repository_1 = require("@crewx/sdk/repository");
|
|
61
61
|
const sdk_1 = require("@crewx/sdk");
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5분
|
|
62
|
+
const cache_db_js_1 = require("../../common/cache-db.js");
|
|
63
|
+
const cached_decorator_js_1 = require("../../common/decorator/cached.decorator.js");
|
|
65
64
|
const CLAUDE_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
66
65
|
const GLM_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
67
66
|
const GROK_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
68
67
|
const CODEX_CACHE_TTL = 5 * 60 * 1000; // 5분
|
|
69
|
-
const AGG_CACHE_TTL_MS =
|
|
70
|
-
const AGG_CACHE_MAX_SIZE = 64;
|
|
68
|
+
const AGG_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
71
69
|
// WI-20260718-002: exponential backoff for negative (failed) provider caches.
|
|
72
70
|
// 30s → 1m → 2m → 5m (max), reset to step 0 on the next success.
|
|
73
71
|
const FAIL_BACKOFF_STEPS_MS = [30 * 1000, 60 * 1000, 2 * 60 * 1000, 5 * 60 * 1000];
|
|
@@ -90,9 +88,6 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
90
88
|
projectRepository;
|
|
91
89
|
snapshotRepo;
|
|
92
90
|
logger = new common_1.Logger(UsageService_1.name);
|
|
93
|
-
cache = new Map();
|
|
94
|
-
aggCache = new Map();
|
|
95
|
-
cleanupInterval;
|
|
96
91
|
claudeOAuthCache = createProviderCache();
|
|
97
92
|
glmCache = createProviderCache();
|
|
98
93
|
grokCache = createProviderCache();
|
|
@@ -112,32 +107,22 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
112
107
|
this.taskRepository = taskRepository;
|
|
113
108
|
this.projectRepository = projectRepository;
|
|
114
109
|
this.snapshotRepo = snapshotRepo;
|
|
115
|
-
this.cleanupInterval = setInterval(() => this.evictExpired(), CACHE_CLEANUP_INTERVAL);
|
|
116
|
-
}
|
|
117
|
-
onModuleDestroy() {
|
|
118
|
-
clearInterval(this.cleanupInterval);
|
|
119
110
|
}
|
|
120
111
|
async getDailyUsage(provider, force) {
|
|
121
|
-
const cacheKey = `daily:${provider}`;
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return { data: cached, cached: true };
|
|
126
|
-
}
|
|
112
|
+
const cacheKey = `usage:daily:${provider}`;
|
|
113
|
+
if (force)
|
|
114
|
+
(0, cache_db_js_1.kvDel)(cacheKey);
|
|
115
|
+
const cached = !force && (0, cache_db_js_1.kvHas)(cacheKey);
|
|
127
116
|
const data = await this.loadDaily(provider);
|
|
128
|
-
|
|
129
|
-
return { data, cached: false };
|
|
117
|
+
return { data, cached };
|
|
130
118
|
}
|
|
131
119
|
async getMonthlyUsage(provider, force) {
|
|
132
|
-
const cacheKey = `monthly:${provider}`;
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return { data: cached, cached: true };
|
|
137
|
-
}
|
|
120
|
+
const cacheKey = `usage:monthly:${provider}`;
|
|
121
|
+
if (force)
|
|
122
|
+
(0, cache_db_js_1.kvDel)(cacheKey);
|
|
123
|
+
const cached = !force && (0, cache_db_js_1.kvHas)(cacheKey);
|
|
138
124
|
const data = await this.loadMonthly(provider);
|
|
139
|
-
|
|
140
|
-
return { data, cached: false };
|
|
125
|
+
return { data, cached };
|
|
141
126
|
}
|
|
142
127
|
async loadDaily(provider) {
|
|
143
128
|
const warnings = [];
|
|
@@ -177,10 +162,6 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
177
162
|
}
|
|
178
163
|
// --- USG.AGENT: Agent usage aggregation ---
|
|
179
164
|
getAgentUsage(from, to, workspace) {
|
|
180
|
-
const cacheKey = this.aggCacheKey('agent', from, to, workspace);
|
|
181
|
-
const cached = this.getAggCache(cacheKey);
|
|
182
|
-
if (cached)
|
|
183
|
-
return cached;
|
|
184
165
|
const rows = this.taskRepository.getAgentUsage(from, to, workspace);
|
|
185
166
|
const wsNameMap = this.resolveWorkspaceNames(rows.map((r) => r.workspace_id).filter((id) => id !== null));
|
|
186
167
|
const grandTotalTokens = rows.reduce((sum, r) => sum + r.total_tokens, 0);
|
|
@@ -207,15 +188,10 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
207
188
|
totalActiveDurationMs: rows.reduce((sum, r) => sum + r.active_duration_ms, 0),
|
|
208
189
|
};
|
|
209
190
|
const data = { from, to, agents, totals };
|
|
210
|
-
this.setAggCache(cacheKey, data);
|
|
211
191
|
return data;
|
|
212
192
|
}
|
|
213
193
|
// --- USG.AGENT.TREND: Agent daily trend ---
|
|
214
194
|
getAgentUsageTrend(from, to, tz, workspace) {
|
|
215
|
-
const cacheKey = this.aggCacheKey('trend', from, to, workspace, tz);
|
|
216
|
-
const cached = this.getAggCache(cacheKey);
|
|
217
|
-
if (cached)
|
|
218
|
-
return cached;
|
|
219
195
|
const rows = this.taskRepository.getAgentUsageTrendRaw(from, to, workspace);
|
|
220
196
|
// Group by local date (tz-converted) × agent_id
|
|
221
197
|
const dailyMap = new Map();
|
|
@@ -250,15 +226,10 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
250
226
|
.sort((a, b) => b.totalTokens - a.totalTokens),
|
|
251
227
|
}));
|
|
252
228
|
const data = { from, to, timezone: tz, daily };
|
|
253
|
-
this.setAggCache(cacheKey, data);
|
|
254
229
|
return data;
|
|
255
230
|
}
|
|
256
231
|
// --- USG.PROVIDER: Provider usage aggregation ---
|
|
257
232
|
getProviderUsage(from, to, workspace) {
|
|
258
|
-
const cacheKey = this.aggCacheKey('provider', from, to, workspace);
|
|
259
|
-
const cached = this.getAggCache(cacheKey);
|
|
260
|
-
if (cached)
|
|
261
|
-
return cached;
|
|
262
233
|
const rows = this.taskRepository.getProviderUsage(from, to, workspace);
|
|
263
234
|
const grandTotalTokens = rows.reduce((sum, r) => sum + r.totalTokens, 0);
|
|
264
235
|
const providers = rows.map((r) => ({
|
|
@@ -282,15 +253,10 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
282
253
|
totalActiveDurationMs: rows.reduce((sum, r) => sum + r.activeDurationMs, 0),
|
|
283
254
|
};
|
|
284
255
|
const data = { from, to, providers, totals };
|
|
285
|
-
this.setAggCache(cacheKey, data);
|
|
286
256
|
return data;
|
|
287
257
|
}
|
|
288
258
|
// --- USG.MODEL: Model usage aggregation ---
|
|
289
259
|
getModelUsage(from, to, workspace) {
|
|
290
|
-
const cacheKey = this.aggCacheKey('model', from, to, workspace);
|
|
291
|
-
const cached = this.getAggCache(cacheKey);
|
|
292
|
-
if (cached)
|
|
293
|
-
return cached;
|
|
294
260
|
const rows = this.taskRepository.getModelUsage(from, to, workspace);
|
|
295
261
|
const grandTotalTokens = rows.reduce((sum, r) => sum + r.totalTokens, 0);
|
|
296
262
|
const models = rows
|
|
@@ -317,7 +283,6 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
317
283
|
totalActiveDurationMs: rows.reduce((sum, r) => sum + r.activeDurationMs, 0),
|
|
318
284
|
};
|
|
319
285
|
const data = { from, to, models, totals };
|
|
320
|
-
this.setAggCache(cacheKey, data);
|
|
321
286
|
return data;
|
|
322
287
|
}
|
|
323
288
|
// --- USR: Rework rate (thread consecutive-call heuristic) ---
|
|
@@ -588,77 +553,6 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
588
553
|
}
|
|
589
554
|
return map;
|
|
590
555
|
}
|
|
591
|
-
// --- Cache with size limit ---
|
|
592
|
-
getFromCache(key) {
|
|
593
|
-
const entry = this.cache.get(key);
|
|
594
|
-
if (!entry)
|
|
595
|
-
return null;
|
|
596
|
-
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|
597
|
-
this.cache.delete(key);
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
return entry.data;
|
|
601
|
-
}
|
|
602
|
-
setCache(key, data) {
|
|
603
|
-
if (this.cache.size >= CACHE_MAX_SIZE && !this.cache.has(key)) {
|
|
604
|
-
const oldestKey = this.findOldestKey();
|
|
605
|
-
if (oldestKey)
|
|
606
|
-
this.cache.delete(oldestKey);
|
|
607
|
-
}
|
|
608
|
-
this.cache.set(key, { data, timestamp: Date.now() });
|
|
609
|
-
}
|
|
610
|
-
aggCacheKey(kind, from, to, workspace, tz) {
|
|
611
|
-
return `${kind}:${from}:${to}:${workspace ?? ''}:${tz ?? ''}`;
|
|
612
|
-
}
|
|
613
|
-
getAggCache(key) {
|
|
614
|
-
const entry = this.aggCache.get(key);
|
|
615
|
-
if (!entry)
|
|
616
|
-
return null;
|
|
617
|
-
if (Date.now() - entry.timestamp > AGG_CACHE_TTL_MS) {
|
|
618
|
-
this.aggCache.delete(key);
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
return entry.data;
|
|
622
|
-
}
|
|
623
|
-
setAggCache(key, data) {
|
|
624
|
-
if (this.aggCache.size >= AGG_CACHE_MAX_SIZE && !this.aggCache.has(key)) {
|
|
625
|
-
let oldestKey = null;
|
|
626
|
-
let oldestTime = Infinity;
|
|
627
|
-
for (const [entryKey, entry] of this.aggCache) {
|
|
628
|
-
if (entry.timestamp < oldestTime) {
|
|
629
|
-
oldestTime = entry.timestamp;
|
|
630
|
-
oldestKey = entryKey;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if (oldestKey)
|
|
634
|
-
this.aggCache.delete(oldestKey);
|
|
635
|
-
}
|
|
636
|
-
this.aggCache.set(key, { data, timestamp: Date.now() });
|
|
637
|
-
}
|
|
638
|
-
findOldestKey() {
|
|
639
|
-
let oldestKey = null;
|
|
640
|
-
let oldestTime = Infinity;
|
|
641
|
-
for (const [key, entry] of this.cache) {
|
|
642
|
-
if (entry.timestamp < oldestTime) {
|
|
643
|
-
oldestTime = entry.timestamp;
|
|
644
|
-
oldestKey = key;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return oldestKey;
|
|
648
|
-
}
|
|
649
|
-
evictExpired() {
|
|
650
|
-
const now = Date.now();
|
|
651
|
-
for (const [key, entry] of this.cache) {
|
|
652
|
-
if (now - entry.timestamp > CACHE_TTL) {
|
|
653
|
-
this.cache.delete(key);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
for (const [key, entry] of this.aggCache) {
|
|
657
|
-
if (now - entry.timestamp > AGG_CACHE_TTL_MS) {
|
|
658
|
-
this.aggCache.delete(key);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
556
|
// --- USG.LIMITS: Subscription limit windows ---
|
|
663
557
|
async getSubscriptionLimits() {
|
|
664
558
|
const [codex, claude, glm, grok] = await Promise.all([
|
|
@@ -792,7 +686,7 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
792
686
|
return { fromIso: from.toISOString(), toIso };
|
|
793
687
|
}
|
|
794
688
|
async parseCodexLimits() {
|
|
795
|
-
return this.getCachedWithDedupe(this.codexCache, CODEX_CACHE_TTL, () => this.computeCodexLimits());
|
|
689
|
+
return this.getCachedWithDedupe(this.codexCache, CODEX_CACHE_TTL, () => this.computeCodexLimits(), 'codex');
|
|
796
690
|
}
|
|
797
691
|
computeCodexLimits() {
|
|
798
692
|
// Tracks whether a session file was actually found this call — only a found-but-unreadable
|
|
@@ -1000,7 +894,7 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
1000
894
|
return { usedPercent: Math.round(usedPercent), resetsAt };
|
|
1001
895
|
}
|
|
1002
896
|
async parseClaudeLimits() {
|
|
1003
|
-
return this.getCachedWithDedupe(this.claudeOAuthCache, CLAUDE_OAUTH_CACHE_TTL, () => this.computeClaudeLimits());
|
|
897
|
+
return this.getCachedWithDedupe(this.claudeOAuthCache, CLAUDE_OAUTH_CACHE_TTL, () => this.computeClaudeLimits(), 'claude');
|
|
1004
898
|
}
|
|
1005
899
|
async computeClaudeLimits() {
|
|
1006
900
|
try {
|
|
@@ -1071,7 +965,7 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
1071
965
|
return { fiveHour, sevenDay };
|
|
1072
966
|
}
|
|
1073
967
|
async parseGrokLimits() {
|
|
1074
|
-
return this.getCachedWithDedupe(this.grokCache, GROK_OAUTH_CACHE_TTL, () => this.computeGrokLimits());
|
|
968
|
+
return this.getCachedWithDedupe(this.grokCache, GROK_OAUTH_CACHE_TTL, () => this.computeGrokLimits(), 'grok');
|
|
1075
969
|
}
|
|
1076
970
|
async computeGrokLimits() {
|
|
1077
971
|
try {
|
|
@@ -1202,7 +1096,7 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
1202
1096
|
return Math.min(100, Math.max(0, Math.round(value)));
|
|
1203
1097
|
}
|
|
1204
1098
|
async parseGlmLimits() {
|
|
1205
|
-
return this.getCachedWithDedupe(this.glmCache, GLM_OAUTH_CACHE_TTL, () => this.computeGlmLimits());
|
|
1099
|
+
return this.getCachedWithDedupe(this.glmCache, GLM_OAUTH_CACHE_TTL, () => this.computeGlmLimits(), 'glm');
|
|
1206
1100
|
}
|
|
1207
1101
|
async computeGlmLimits() {
|
|
1208
1102
|
try {
|
|
@@ -1262,26 +1156,36 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
1262
1156
|
* - Concurrent callers hitting a cache miss share the same in-flight promise instead of
|
|
1263
1157
|
* firing duplicate external calls.
|
|
1264
1158
|
*/
|
|
1265
|
-
async getCachedWithDedupe(cache, successTtl, fetcher) {
|
|
1159
|
+
async getCachedWithDedupe(cache, successTtl, fetcher, provider) {
|
|
1266
1160
|
const now = Date.now();
|
|
1267
1161
|
if (!cache.inFlight && !this.isProviderCacheValid(cache, now, successTtl)) {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
.
|
|
1271
|
-
cache.
|
|
1272
|
-
cache.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1162
|
+
const persisted = (0, cache_db_js_1.kvGet)(`usage:limits:${provider}`);
|
|
1163
|
+
if (persisted !== undefined) {
|
|
1164
|
+
cache.data = persisted;
|
|
1165
|
+
cache.ts = now;
|
|
1166
|
+
cache.failCount = 0;
|
|
1167
|
+
cache.lastGood = persisted;
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
cache.inFlight = Promise.resolve()
|
|
1171
|
+
.then(() => fetcher())
|
|
1172
|
+
.then((result) => {
|
|
1173
|
+
cache.data = result;
|
|
1174
|
+
cache.ts = Date.now();
|
|
1175
|
+
if (result !== null) {
|
|
1176
|
+
cache.failCount = 0;
|
|
1177
|
+
cache.lastGood = result;
|
|
1178
|
+
(0, cache_db_js_1.kvSet)(`usage:limits:${provider}`, result, successTtl);
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
cache.failCount += 1;
|
|
1182
|
+
}
|
|
1183
|
+
return result;
|
|
1184
|
+
})
|
|
1185
|
+
.finally(() => {
|
|
1186
|
+
cache.inFlight = null;
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1285
1189
|
}
|
|
1286
1190
|
if (cache.inFlight) {
|
|
1287
1191
|
const result = await cache.inFlight;
|
|
@@ -1311,6 +1215,42 @@ let UsageService = UsageService_1 = class UsageService {
|
|
|
1311
1215
|
}
|
|
1312
1216
|
};
|
|
1313
1217
|
exports.UsageService = UsageService;
|
|
1218
|
+
__decorate([
|
|
1219
|
+
(0, cached_decorator_js_1.Cached)({ key: 'usage:daily:{0}', ttlMs: 5 * 60 * 1000 }),
|
|
1220
|
+
__metadata("design:type", Function),
|
|
1221
|
+
__metadata("design:paramtypes", [String]),
|
|
1222
|
+
__metadata("design:returntype", Promise)
|
|
1223
|
+
], UsageService.prototype, "loadDaily", null);
|
|
1224
|
+
__decorate([
|
|
1225
|
+
(0, cached_decorator_js_1.Cached)({ key: 'usage:monthly:{0}', ttlMs: 5 * 60 * 1000 }),
|
|
1226
|
+
__metadata("design:type", Function),
|
|
1227
|
+
__metadata("design:paramtypes", [String]),
|
|
1228
|
+
__metadata("design:returntype", Promise)
|
|
1229
|
+
], UsageService.prototype, "loadMonthly", null);
|
|
1230
|
+
__decorate([
|
|
1231
|
+
(0, cached_decorator_js_1.Cached)({ key: 'usage:agg:agent:{0}:{1}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
|
|
1232
|
+
__metadata("design:type", Function),
|
|
1233
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
1234
|
+
__metadata("design:returntype", Object)
|
|
1235
|
+
], UsageService.prototype, "getAgentUsage", null);
|
|
1236
|
+
__decorate([
|
|
1237
|
+
(0, cached_decorator_js_1.Cached)({ key: 'usage:agg:trend:{0}:{1}:{3}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
|
|
1238
|
+
__metadata("design:type", Function),
|
|
1239
|
+
__metadata("design:paramtypes", [String, String, String, String]),
|
|
1240
|
+
__metadata("design:returntype", Object)
|
|
1241
|
+
], UsageService.prototype, "getAgentUsageTrend", null);
|
|
1242
|
+
__decorate([
|
|
1243
|
+
(0, cached_decorator_js_1.Cached)({ key: 'usage:agg:provider:{0}:{1}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
|
|
1244
|
+
__metadata("design:type", Function),
|
|
1245
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
1246
|
+
__metadata("design:returntype", Object)
|
|
1247
|
+
], UsageService.prototype, "getProviderUsage", null);
|
|
1248
|
+
__decorate([
|
|
1249
|
+
(0, cached_decorator_js_1.Cached)({ key: 'usage:agg:model:{0}:{1}:{2}', ttlMs: AGG_CACHE_TTL_MS }),
|
|
1250
|
+
__metadata("design:type", Function),
|
|
1251
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
1252
|
+
__metadata("design:returntype", Object)
|
|
1253
|
+
], UsageService.prototype, "getModelUsage", null);
|
|
1314
1254
|
exports.UsageService = UsageService = UsageService_1 = __decorate([
|
|
1315
1255
|
(0, common_1.Injectable)(),
|
|
1316
1256
|
__metadata("design:paramtypes", [task_repository_js_1.TaskRepository,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewx",
|
|
3
|
-
"version": "0.9.0-rc.
|
|
3
|
+
"version": "0.9.0-rc.64",
|
|
4
4
|
"description": "CrewX — AI agent team dashboard with Electron UI and CLI (Web + Electron + Global CLI)",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -82,20 +82,20 @@
|
|
|
82
82
|
"wink-nlp-utils": "2.1.0",
|
|
83
83
|
"yargs": "17.7.0",
|
|
84
84
|
"zod": "3.25.76",
|
|
85
|
-
"@crewx/chromex": "0.1.0-rc.
|
|
86
|
-
"@crewx/
|
|
87
|
-
"@crewx/
|
|
88
|
-
"@crewx/doc": "0.1.9-rc.
|
|
89
|
-
"@crewx/dreaming": "0.1.0-rc.
|
|
90
|
-
"@crewx/
|
|
91
|
-
"@crewx/memory": "0.1.23-rc.
|
|
92
|
-
"@crewx/
|
|
93
|
-
"@crewx/
|
|
85
|
+
"@crewx/chromex": "0.1.0-rc.96",
|
|
86
|
+
"@crewx/cron": "0.1.10-rc.98",
|
|
87
|
+
"@crewx/cli": "0.9.0-rc.64",
|
|
88
|
+
"@crewx/doc": "0.1.9-rc.56",
|
|
89
|
+
"@crewx/dreaming": "0.1.0-rc.89",
|
|
90
|
+
"@crewx/knowledge-core": "0.1.17-rc.73",
|
|
91
|
+
"@crewx/memory": "0.1.23-rc.80",
|
|
92
|
+
"@crewx/sdk": "0.9.0-rc.64",
|
|
93
|
+
"@crewx/search": "0.1.10-rc.59",
|
|
94
94
|
"@crewx/shared": "0.0.6",
|
|
95
95
|
"@crewx/skill": "0.1.20",
|
|
96
|
-
"@crewx/wbs": "0.1.10-rc.
|
|
97
|
-
"@crewx/wi": "0.1.10-rc.
|
|
98
|
-
"@crewx/workflow": "0.3.22-rc.
|
|
96
|
+
"@crewx/wbs": "0.1.10-rc.89",
|
|
97
|
+
"@crewx/wi": "0.1.10-rc.84",
|
|
98
|
+
"@crewx/workflow": "0.3.22-rc.110"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@anthropic-ai/claude-agent-sdk": "0.2.132",
|