mio-agent-runtime 0.5.17 → 0.5.23
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/README.md +188 -107
- package/adapters/claude-code.js +141 -0
- package/bin/mio.js +526 -245
- package/observe/observer.js +326 -4
- package/package.json +10 -3
- package/scripts/verify-packed-runtime.js +23 -8
- package/server/digest.js +278 -0
- package/server/host-capabilities.js +5 -0
- package/server/memory-store.js +376 -0
- package/server/mio-intelligence-mcp/index.js +2383 -2471
- package/server/retention.js +150 -0
package/observe/observer.js
CHANGED
|
@@ -65,6 +65,30 @@ function stateFile(home) {
|
|
|
65
65
|
return path.join(home, 'observe-state.json')
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Drop repeated lines from stored context lists, preserving the most recent
|
|
69
|
+
// occurrence order. Older builds could write the same line into every slot of
|
|
70
|
+
// a workspace block (a generated digest headline broadcast to all projects),
|
|
71
|
+
// so existing observe-state files still carry duplicates until they are
|
|
72
|
+
// rewritten. Normalising on load heals that data without a migration step.
|
|
73
|
+
function dedupeContexts(contexts) {
|
|
74
|
+
const out = {}
|
|
75
|
+
for (const [cwd, list] of Object.entries(contexts)) {
|
|
76
|
+
if (!Array.isArray(list)) {
|
|
77
|
+
out[cwd] = []
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const seen = new Set()
|
|
81
|
+
const deduped = []
|
|
82
|
+
for (const line of list) {
|
|
83
|
+
if (seen.has(line)) continue
|
|
84
|
+
seen.add(line)
|
|
85
|
+
deduped.push(line)
|
|
86
|
+
}
|
|
87
|
+
out[cwd] = deduped
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
function loadState(home) {
|
|
69
93
|
try {
|
|
70
94
|
const parsed = JSON.parse(fs.readFileSync(stateFile(home), 'utf8'))
|
|
@@ -74,7 +98,7 @@ function loadState(home) {
|
|
|
74
98
|
files: parsed.files || {},
|
|
75
99
|
buffers: parsed.buffers || {},
|
|
76
100
|
openTurns: parsed.openTurns || {},
|
|
77
|
-
contexts: parsed.contexts || {},
|
|
101
|
+
contexts: dedupeContexts(parsed.contexts || {}),
|
|
78
102
|
codexFiles: parsed.codexFiles || {},
|
|
79
103
|
codexBuffers: parsed.codexBuffers || {},
|
|
80
104
|
codexTurns: parsed.codexTurns || {},
|
|
@@ -86,6 +110,9 @@ function loadState(home) {
|
|
|
86
110
|
hmSeeded: parsed.hmSeeded || false,
|
|
87
111
|
hmLastMsgId: parsed.hmLastMsgId || 0,
|
|
88
112
|
hmTurns: parsed.hmTurns || {},
|
|
113
|
+
clFiles: parsed.clFiles || {},
|
|
114
|
+
clBuffers: parsed.clBuffers || {},
|
|
115
|
+
clTurns: parsed.clTurns || {},
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
118
|
} catch (_) {}
|
|
@@ -106,6 +133,9 @@ function loadState(home) {
|
|
|
106
133
|
hmSeeded: false,
|
|
107
134
|
hmLastMsgId: 0,
|
|
108
135
|
hmTurns: {},
|
|
136
|
+
clFiles: {},
|
|
137
|
+
clBuffers: {},
|
|
138
|
+
clTurns: {},
|
|
109
139
|
}
|
|
110
140
|
}
|
|
111
141
|
|
|
@@ -282,10 +312,23 @@ const CONTEXT_MAX = 6
|
|
|
282
312
|
// Maintain a compact 'recent Mio context' block inside <workspace>/AGENTS.md,
|
|
283
313
|
// which WorkBuddy's MemoryLoader auto-injects into every session. This makes
|
|
284
314
|
// retrieval guaranteed (not dependent on the model deciding to read memory).
|
|
285
|
-
function updateProjectContext(state, cwd, line) {
|
|
315
|
+
function updateProjectContext(state, cwd, line, targetFile) {
|
|
286
316
|
if (!cwd || !line) return
|
|
287
|
-
const target = path.join(cwd, 'AGENTS.md')
|
|
288
|
-
|
|
317
|
+
const target = path.join(cwd, targetFile || 'AGENTS.md')
|
|
318
|
+
// Normalise the whole list before inserting, not just the incoming line.
|
|
319
|
+
// Callers may load state without going through loadState (the digest
|
|
320
|
+
// write-back reads the file directly), so stale duplicates have to be
|
|
321
|
+
// cleared here or they keep consuming slots and push real history out.
|
|
322
|
+
const list = []
|
|
323
|
+
const seen = new Set()
|
|
324
|
+
for (const item of state.contexts[cwd] || []) {
|
|
325
|
+
if (seen.has(item)) continue
|
|
326
|
+
seen.add(item)
|
|
327
|
+
list.push(item)
|
|
328
|
+
}
|
|
329
|
+
// Re-inserting an existing line moves it to the front instead of duplicating.
|
|
330
|
+
const existing = list.indexOf(line)
|
|
331
|
+
if (existing !== -1) list.splice(existing, 1)
|
|
289
332
|
list.unshift(line)
|
|
290
333
|
if (list.length > CONTEXT_MAX) list.pop()
|
|
291
334
|
state.contexts[cwd] = list
|
|
@@ -1133,6 +1176,275 @@ function runHermesCycle(home, state, sinks, opts) {
|
|
|
1133
1176
|
}
|
|
1134
1177
|
|
|
1135
1178
|
|
|
1179
|
+
// ---------------------------------------------------------------------------
|
|
1180
|
+
// Claude Code session observation (passive, transcript JSONL tailing)
|
|
1181
|
+
// ---------------------------------------------------------------------------
|
|
1182
|
+
|
|
1183
|
+
// Claude Code writes append-only JSONL transcripts under
|
|
1184
|
+
// <configDir>/projects/<munged-cwd>/<sessionId>.jsonl. Each line carries
|
|
1185
|
+
// {type, message:{role, content}, cwd, sessionId, timestamp, uuid, ...}.
|
|
1186
|
+
// Assistant content items are text/thinking/tool_use; tool results come back
|
|
1187
|
+
// as user lines whose content array holds tool_result items with is_error.
|
|
1188
|
+
// A user prompt line (string content or a text item) closes the previous
|
|
1189
|
+
// turn; sidechain lines (subagent transcripts) are skipped as noise.
|
|
1190
|
+
|
|
1191
|
+
function claudeConfigDir() {
|
|
1192
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function claudeProjectsDir() {
|
|
1196
|
+
return path.join(claudeConfigDir(), 'projects')
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function listClaudeTranscripts() {
|
|
1200
|
+
const root = claudeProjectsDir()
|
|
1201
|
+
const out = []
|
|
1202
|
+
if (!fs.existsSync(root)) return out
|
|
1203
|
+
const walk = (dir) => {
|
|
1204
|
+
let entries
|
|
1205
|
+
try {
|
|
1206
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1207
|
+
} catch (_) {
|
|
1208
|
+
return
|
|
1209
|
+
}
|
|
1210
|
+
for (const entry of entries) {
|
|
1211
|
+
const full = path.join(dir, entry.name)
|
|
1212
|
+
if (entry.isDirectory()) walk(full)
|
|
1213
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full)
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
walk(root)
|
|
1217
|
+
return out
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function createClaudeTurn(meta) {
|
|
1221
|
+
meta = meta || {}
|
|
1222
|
+
return {
|
|
1223
|
+
id: null,
|
|
1224
|
+
ts: null,
|
|
1225
|
+
sessionId: meta.sessionId || null,
|
|
1226
|
+
cwd: meta.cwd || null,
|
|
1227
|
+
project: meta.cwd ? path.basename(meta.cwd) : 'unknown',
|
|
1228
|
+
lastText: null,
|
|
1229
|
+
toolCount: 0,
|
|
1230
|
+
errors: [],
|
|
1231
|
+
apiError: false,
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function claudeContentItems(obj) {
|
|
1236
|
+
const content = obj && obj.message && obj.message.content
|
|
1237
|
+
if (typeof content === 'string') return [{ type: 'text', text: content }]
|
|
1238
|
+
return Array.isArray(content) ? content.filter(Boolean) : []
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function finishClaudeTurn(turn, outcome) {
|
|
1242
|
+
return {
|
|
1243
|
+
id: turn.id,
|
|
1244
|
+
ts: turn.ts,
|
|
1245
|
+
sessionId: turn.sessionId,
|
|
1246
|
+
cwd: turn.cwd,
|
|
1247
|
+
project: turn.project,
|
|
1248
|
+
lastText: turn.lastText,
|
|
1249
|
+
toolCount: turn.toolCount,
|
|
1250
|
+
errors: turn.errors.slice(),
|
|
1251
|
+
apiError: turn.apiError,
|
|
1252
|
+
outcome: outcome,
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function feedClaudeLine(turn, obj) {
|
|
1257
|
+
if (!obj || typeof obj !== 'object') return { next: turn }
|
|
1258
|
+
// Subagent (sidechain) transcripts duplicate main-thread work; skip them.
|
|
1259
|
+
if (obj.isSidechain === true) return { next: turn }
|
|
1260
|
+
if (obj.sessionId) turn.sessionId = obj.sessionId
|
|
1261
|
+
if (obj.cwd) {
|
|
1262
|
+
turn.cwd = obj.cwd
|
|
1263
|
+
turn.project = path.basename(obj.cwd)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (obj.type === 'user') {
|
|
1267
|
+
const items = claudeContentItems(obj)
|
|
1268
|
+
let hasToolError = false
|
|
1269
|
+
for (const item of items) {
|
|
1270
|
+
if (item.type === 'tool_result' && item.is_error) {
|
|
1271
|
+
turn.errors.push({ toolUseId: item.tool_use_id || 'unknown' })
|
|
1272
|
+
hasToolError = true
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (turn.errors.length > 20) turn.errors.splice(0, turn.errors.length - 20)
|
|
1276
|
+
|
|
1277
|
+
// tool_result-only user lines continue the current turn; a string content
|
|
1278
|
+
// or text item means a fresh prompt, which closes the previous turn.
|
|
1279
|
+
const isPrompt =
|
|
1280
|
+
typeof (obj.message && obj.message.content) === 'string' ||
|
|
1281
|
+
items.some((item) => item.type === 'text')
|
|
1282
|
+
if (isPrompt) {
|
|
1283
|
+
let finalized = null
|
|
1284
|
+
if (turn.id) {
|
|
1285
|
+
finalized = finishClaudeTurn(turn, turn.apiError ? 'failure' : turn.lastText ? 'success' : 'aborted')
|
|
1286
|
+
}
|
|
1287
|
+
const next = createClaudeTurn({ sessionId: turn.sessionId, cwd: turn.cwd })
|
|
1288
|
+
next.id = obj.uuid || null
|
|
1289
|
+
next.ts = obj.timestamp ? Date.parse(obj.timestamp) || Date.now() : Date.now()
|
|
1290
|
+
return { finalized, next }
|
|
1291
|
+
}
|
|
1292
|
+
return { next: turn }
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (obj.type === 'assistant') {
|
|
1296
|
+
for (const item of claudeContentItems(obj)) {
|
|
1297
|
+
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
|
|
1298
|
+
turn.lastText = item.text
|
|
1299
|
+
} else if (item.type === 'tool_use') {
|
|
1300
|
+
turn.toolCount += 1
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (obj.isApiErrorMessage === true || obj.error) turn.apiError = true
|
|
1304
|
+
const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN
|
|
1305
|
+
if (Number.isFinite(ts)) turn.ts = ts
|
|
1306
|
+
return { next: turn }
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return { next: turn }
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function emitClaudeOutcome(turn, sinks, state) {
|
|
1313
|
+
if (!turn.id) return false
|
|
1314
|
+
const summary = cleanText(turn.lastText)
|
|
1315
|
+
if (!summary && !turn.apiError) return false
|
|
1316
|
+
const outcome = turn.apiError && turn.outcome !== 'failure' ? 'failure' : turn.outcome
|
|
1317
|
+
const turnToken = String(turn.id).slice(0, 8)
|
|
1318
|
+
const tsToken = turn.ts ? Math.floor(turn.ts / 1000) : Math.floor(Date.now() / 1000)
|
|
1319
|
+
|
|
1320
|
+
sinks.trace({
|
|
1321
|
+
trace_id: 'claude:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + tsToken,
|
|
1322
|
+
event_type: 'task_outcome',
|
|
1323
|
+
outcome: outcome,
|
|
1324
|
+
payload: {
|
|
1325
|
+
host: 'claude-code',
|
|
1326
|
+
project: turn.project,
|
|
1327
|
+
sessionId: turn.sessionId,
|
|
1328
|
+
turnId: turn.id,
|
|
1329
|
+
toolCalls: turn.toolCount,
|
|
1330
|
+
errors: turn.errors.length,
|
|
1331
|
+
summary: summary.slice(0, 300),
|
|
1332
|
+
source: 'claude-observer',
|
|
1333
|
+
},
|
|
1334
|
+
agent: 'claude-code',
|
|
1335
|
+
host: 'claude-code',
|
|
1336
|
+
project: turn.project,
|
|
1337
|
+
})
|
|
1338
|
+
|
|
1339
|
+
if (outcome === 'success' && summary.length >= 10) {
|
|
1340
|
+
sinks.memory({
|
|
1341
|
+
kind: 'note',
|
|
1342
|
+
content: '[claude-code] 任务完成: ' + summary.slice(0, 160),
|
|
1343
|
+
tags: ['task-outcome', 'claude-code', turn.project],
|
|
1344
|
+
project: turn.project,
|
|
1345
|
+
source: 'claude-observer',
|
|
1346
|
+
})
|
|
1347
|
+
// Claude Code auto-loads <workspace>/CLAUDE.md, so write the context
|
|
1348
|
+
// block there instead of AGENTS.md.
|
|
1349
|
+
if (turn.cwd) {
|
|
1350
|
+
var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
|
|
1351
|
+
updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80), 'CLAUDE.md')
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
for (const err of turn.errors) {
|
|
1356
|
+
sinks.trace({
|
|
1357
|
+
trace_id: 'claude:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + String(err.toolUseId || 'err').slice(0, 12),
|
|
1358
|
+
event_type: 'error',
|
|
1359
|
+
outcome: 'error',
|
|
1360
|
+
payload: {
|
|
1361
|
+
host: 'claude-code',
|
|
1362
|
+
project: turn.project,
|
|
1363
|
+
sessionId: turn.sessionId,
|
|
1364
|
+
turnId: turn.id,
|
|
1365
|
+
toolUseId: err.toolUseId,
|
|
1366
|
+
},
|
|
1367
|
+
agent: 'claude-code',
|
|
1368
|
+
host: 'claude-code',
|
|
1369
|
+
project: turn.project,
|
|
1370
|
+
})
|
|
1371
|
+
}
|
|
1372
|
+
return true
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function runClaudeCycle(home, state, sinks, opts) {
|
|
1376
|
+
const out = { files: 0, lines: 0, outcomes: 0 }
|
|
1377
|
+
const files = listClaudeTranscripts()
|
|
1378
|
+
const now = Date.now()
|
|
1379
|
+
const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
|
|
1380
|
+
|
|
1381
|
+
for (const file of files) {
|
|
1382
|
+
out.files += 1
|
|
1383
|
+
let st
|
|
1384
|
+
try {
|
|
1385
|
+
st = fs.statSync(file)
|
|
1386
|
+
} catch (_) {
|
|
1387
|
+
continue
|
|
1388
|
+
}
|
|
1389
|
+
const info = state.clFiles[file] || { size: 0 }
|
|
1390
|
+
if (!state.clFiles[file] && now - st.mtimeMs > maxAgeMs) continue
|
|
1391
|
+
const size = st.size
|
|
1392
|
+
if (size < info.size) {
|
|
1393
|
+
info.size = 0
|
|
1394
|
+
state.clBuffers[file] = ''
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
let turn = state.clTurns[file] || createClaudeTurn()
|
|
1398
|
+
|
|
1399
|
+
if (size > info.size) {
|
|
1400
|
+
let chunk = ''
|
|
1401
|
+
try {
|
|
1402
|
+
const fd = fs.openSync(file, 'r')
|
|
1403
|
+
const buf = Buffer.alloc(size - info.size)
|
|
1404
|
+
fs.readSync(fd, buf, 0, buf.length, info.size)
|
|
1405
|
+
fs.closeSync(fd)
|
|
1406
|
+
chunk = buf.toString('utf8')
|
|
1407
|
+
} catch (_) {
|
|
1408
|
+
continue
|
|
1409
|
+
}
|
|
1410
|
+
const pending = (state.clBuffers[file] || '') + chunk
|
|
1411
|
+
const lines = pending.split(/\r?\n/)
|
|
1412
|
+
state.clBuffers[file] = lines.pop()
|
|
1413
|
+
for (const line of lines) {
|
|
1414
|
+
if (!line.trim()) continue
|
|
1415
|
+
out.lines += 1
|
|
1416
|
+
let obj
|
|
1417
|
+
try {
|
|
1418
|
+
obj = JSON.parse(line)
|
|
1419
|
+
} catch (_) {
|
|
1420
|
+
continue
|
|
1421
|
+
}
|
|
1422
|
+
const result = feedClaudeLine(turn, obj)
|
|
1423
|
+
turn = result.next
|
|
1424
|
+
if (result.finalized) {
|
|
1425
|
+
if (emitClaudeOutcome(result.finalized, sinks, state)) out.outcomes += 1
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
state.clFiles[file] = { size: size, mtimeMs: st.mtimeMs }
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Idle finalize: the session has answered but no new prompt arrived yet.
|
|
1432
|
+
if (turn.id && turn.lastText && now - st.mtimeMs > IDLE_MS) {
|
|
1433
|
+
if (emitClaudeOutcome(finishClaudeTurn(turn, turn.apiError ? 'failure' : 'success'), sinks, state)) {
|
|
1434
|
+
out.outcomes += 1
|
|
1435
|
+
}
|
|
1436
|
+
state.clTurns[file] = null
|
|
1437
|
+
} else if (turn.id) {
|
|
1438
|
+
state.clTurns[file] = turn
|
|
1439
|
+
} else {
|
|
1440
|
+
state.clTurns[file] = null
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
return out
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
|
|
1136
1448
|
// ---------------------------------------------------------------------------
|
|
1137
1449
|
// cycle
|
|
1138
1450
|
// ---------------------------------------------------------------------------
|
|
@@ -1225,6 +1537,11 @@ function runCycle(home, state, sinks, opts) {
|
|
|
1225
1537
|
out.lines += hmRes.lines
|
|
1226
1538
|
out.outcomes += hmRes.outcomes
|
|
1227
1539
|
|
|
1540
|
+
const clRes = runClaudeCycle(home, state, sinks, opts)
|
|
1541
|
+
out.files += clRes.files
|
|
1542
|
+
out.lines += clRes.lines
|
|
1543
|
+
out.outcomes += clRes.outcomes
|
|
1544
|
+
|
|
1228
1545
|
saveState(home, state)
|
|
1229
1546
|
return out
|
|
1230
1547
|
}
|
|
@@ -1311,12 +1628,17 @@ module.exports = {
|
|
|
1311
1628
|
workbuddyConfigDir,
|
|
1312
1629
|
loadState,
|
|
1313
1630
|
saveState,
|
|
1631
|
+
dedupeContexts,
|
|
1314
1632
|
updateProjectContext,
|
|
1315
1633
|
runCycle,
|
|
1316
1634
|
opencodeDbPath,
|
|
1317
1635
|
runOpencodeCycle,
|
|
1318
1636
|
runHermesCycle,
|
|
1319
1637
|
hermesDbPath,
|
|
1638
|
+
claudeProjectsDir,
|
|
1639
|
+
runClaudeCycle,
|
|
1640
|
+
createClaudeTurn,
|
|
1641
|
+
feedClaudeLine,
|
|
1320
1642
|
createSinks,
|
|
1321
1643
|
startForeground,
|
|
1322
1644
|
startBackground,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"!server/mio-intelligence-mcp/__tests__"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check server/runtime-modules.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
|
|
20
|
+
"check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check adapters/claude-code.js && node --check server/runtime-modules.js && node --check server/memory-store.js && node --check server/retention.js && node --check server/digest.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
|
|
21
|
+
"test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
|
|
21
22
|
"prepack": "npm run check",
|
|
22
23
|
"verify:pack-install": "node scripts/verify-packed-runtime.js"
|
|
23
24
|
},
|
|
@@ -34,5 +35,11 @@
|
|
|
34
35
|
"@akemi-mio/evolution-scheduler": "^0.1.0",
|
|
35
36
|
"@akemi-mio/observer": "^0.1.0",
|
|
36
37
|
"@akemi-mio/insight": "^0.1.0"
|
|
37
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"main": "index.js",
|
|
40
|
+
"directories": {
|
|
41
|
+
"doc": "docs"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [],
|
|
44
|
+
"author": ""
|
|
38
45
|
}
|
|
@@ -76,12 +76,13 @@ function npmCommand(platform = process.platform) {
|
|
|
76
76
|
|
|
77
77
|
function npmInvocation(args, env = process.env, platform = process.platform, node = process.execPath) {
|
|
78
78
|
if (env.npm_execpath) {
|
|
79
|
-
return { command: node, args: [env.npm_execpath, ...args] }
|
|
79
|
+
return { command: node, args: [env.npm_execpath, ...args], shell: false }
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
// Fall back to the npm shim on PATH instead of routing through cmd.exe.
|
|
82
|
+
// cmd.exe is unavailable in some sandboxes, and a bare .cmd/.bat shim cannot
|
|
83
|
+
// be spawned directly (Node requires shell:true for those on Windows), so
|
|
84
|
+
// signal the shell requirement to the runner instead of guessing there.
|
|
85
|
+
return { command: npmCommand(platform), args: [...args], shell: platform === 'win32' }
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function packageDirName(name) {
|
|
@@ -95,9 +96,12 @@ function tarballFileName(name, version) {
|
|
|
95
96
|
|
|
96
97
|
function readPackageJson(workspaceRoot, name) {
|
|
97
98
|
const packageDir = path.join(workspaceRoot, 'packages', packageDirName(name))
|
|
99
|
+
// Windows editors may write package.json with a UTF-8 BOM; JSON.parse
|
|
100
|
+
// rejects the leading U+FEFF, so strip it before parsing.
|
|
101
|
+
const raw = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
|
|
98
102
|
return {
|
|
99
103
|
packageDir,
|
|
100
|
-
manifest: JSON.parse(
|
|
104
|
+
manifest: JSON.parse(raw),
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -211,6 +215,11 @@ function run(command, args, options = {}) {
|
|
|
211
215
|
env: options.env || process.env,
|
|
212
216
|
encoding: 'utf8',
|
|
213
217
|
stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
|
|
218
|
+
// Windows .cmd/.bat shims (npm.cmd) cannot be spawned directly by
|
|
219
|
+
// execFileSync since Node's CVE-2024-27980 hardening; they need a shell.
|
|
220
|
+
// This only kicks in for the npm shim path — node-based invocations pass
|
|
221
|
+
// an absolute node executable and still run shell-free.
|
|
222
|
+
shell: options.shell === true,
|
|
214
223
|
})
|
|
215
224
|
return output ? output.trim() : ''
|
|
216
225
|
}
|
|
@@ -296,14 +305,19 @@ function verifyPackedRuntimeInstall(input = {}) {
|
|
|
296
305
|
run(pack.command, pack.args, {
|
|
297
306
|
cwd: pkg.packageDir,
|
|
298
307
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
308
|
+
shell: pack.shell,
|
|
299
309
|
})
|
|
300
310
|
packageArtifactEvidence(pkg)
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
const init = npmInvocation(['init', '-y'])
|
|
304
|
-
run(init.command, init.args, { cwd: plan.installDir, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
314
|
+
run(init.command, init.args, { cwd: plan.installDir, stdio: ['ignore', 'pipe', 'pipe'], shell: init.shell })
|
|
305
315
|
const install = npmInvocation(plan.install.args)
|
|
306
|
-
run(install.command, install.args, {
|
|
316
|
+
run(install.command, install.args, {
|
|
317
|
+
cwd: plan.install.cwd,
|
|
318
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
319
|
+
shell: install.shell,
|
|
320
|
+
})
|
|
307
321
|
const smokeEnv = {
|
|
308
322
|
...process.env,
|
|
309
323
|
MIO_HOME: path.join(plan.installDir, '.mio-home'),
|
|
@@ -360,6 +374,7 @@ module.exports = {
|
|
|
360
374
|
createVerifyPackedRuntimeOptions,
|
|
361
375
|
createPackedRuntimeInstallPlan,
|
|
362
376
|
packageArtifactEvidence,
|
|
377
|
+
readPackageJson,
|
|
363
378
|
npmInvocation,
|
|
364
379
|
npmCommand,
|
|
365
380
|
parseVerifyPackedRuntimeArgs,
|