@claude-flow/cli 3.56.6 → 3.56.8
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/.claude/helpers/auto-memory-hook.mjs +109 -10
- package/.claude/helpers/helpers.manifest.json +4 -4
- package/.claude/helpers/intelligence.cjs +57 -4
- package/README.md +9 -3
- package/bin/cli.js +11 -1
- package/bin/mcp-server.js +11 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/coverage-reader.d.ts +29 -0
- package/dist/src/commands/coverage-reader.js +247 -0
- package/dist/src/commands/doctor-native-addon.d.ts +23 -0
- package/dist/src/commands/doctor-native-addon.js +135 -0
- package/dist/src/commands/doctor.js +3 -0
- package/dist/src/commands/hooks.js +17 -181
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.d.ts +2 -0
- package/dist/src/commands/init.js +37 -5
- package/dist/src/commands/mods.d.ts +3 -0
- package/dist/src/commands/mods.js +19 -5
- package/dist/src/commands/tui.d.ts +12 -0
- package/dist/src/commands/tui.js +98 -0
- package/dist/src/fs-secure.d.ts +25 -0
- package/dist/src/fs-secure.js +51 -2
- package/dist/src/init/executor.js +11 -14
- package/dist/src/init/helper-commands.d.ts +2 -0
- package/dist/src/init/helper-commands.js +2 -0
- package/dist/src/init/helpers-generator.js +4 -1
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-server.js +10 -2
- package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/dist/src/mcp-tools/capability-brain.js +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +26 -4
- package/dist/src/mcp-tools/memory-tools.js +2 -1
- package/dist/src/memory/live-memory-row.d.ts +3 -1
- package/dist/src/memory/live-memory-row.js +3 -1
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +3 -2
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +12 -11
- package/dist/src/mods/claude-installs.js +7 -3
- package/dist/src/mods/install.js +32 -12
- package/dist/src/mods/plugin-resolve.js +3 -1
- package/dist/src/mods/probe.js +92 -4
- package/dist/src/services/command-recording.d.ts +17 -0
- package/dist/src/services/command-recording.js +91 -0
- package/dist/src/services/policy-ledger-anchor.js +20 -2
- package/node_modules/@claude-flow/codex/README.md +20 -3
- package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
- package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
- package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/package.json +3 -3
- package/plugins/ruflo-console/dist/tui.mjs +279 -0
|
@@ -1402,7 +1402,8 @@ export const memoryTools = [
|
|
|
1402
1402
|
if (!v.valid)
|
|
1403
1403
|
throw new Error(v.error);
|
|
1404
1404
|
}
|
|
1405
|
-
|
|
1405
|
+
// #4056: TTL hides expired rows from normal reads, so the cleanup has to ask for them.
|
|
1406
|
+
const all = await listEntries({ limit: 100000, namespace, includeExpired: true });
|
|
1406
1407
|
const now = Date.now();
|
|
1407
1408
|
const expired = all.entries.filter(e => {
|
|
1408
1409
|
const exp = e.expiresAt;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** TTL is a read-time visibility rule, independent of background cleanup. */
|
|
2
|
-
export declare function liveMemoryRowSql(
|
|
2
|
+
export declare function liveMemoryRowSql(opts?: {
|
|
3
|
+
includeExpired?: boolean;
|
|
4
|
+
}): string;
|
|
3
5
|
//# sourceMappingURL=live-memory-row.d.ts.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** TTL is a read-time visibility rule, independent of background cleanup. */
|
|
2
|
-
export function liveMemoryRowSql() {
|
|
2
|
+
export function liveMemoryRowSql(opts = {}) {
|
|
3
|
+
if (opts.includeExpired)
|
|
4
|
+
return `(status = 'active' OR status IS NULL)`;
|
|
3
5
|
return `(status = 'active' OR status IS NULL) AND (expires_at IS NULL OR expires_at > ${Date.now()})`;
|
|
4
6
|
}
|
|
5
7
|
//# sourceMappingURL=live-memory-row.js.map
|
|
@@ -142,6 +142,8 @@ export declare function bridgeListEntries(options: {
|
|
|
142
142
|
provenanceFilter?: string[];
|
|
143
143
|
/** ADR-472: include each entry's embedding as int8+scale (`embeddingQ8`); at most MAX_LIST_EMBEDDINGS rows. */
|
|
144
144
|
includeEmbedding?: boolean;
|
|
145
|
+
/** #4056: also return rows whose TTL has passed (with `expiresAt`). For cleanup only. */
|
|
146
|
+
includeExpired?: boolean;
|
|
145
147
|
}): Promise<{
|
|
146
148
|
success: boolean;
|
|
147
149
|
entries: {
|
|
@@ -152,6 +154,8 @@ export declare function bridgeListEntries(options: {
|
|
|
152
154
|
accessCount: number;
|
|
153
155
|
createdAt: string;
|
|
154
156
|
updatedAt: string;
|
|
157
|
+
/** #4056: epoch ms the row expires at; null when it never does. */
|
|
158
|
+
expiresAt?: number | null;
|
|
155
159
|
hasEmbedding: boolean;
|
|
156
160
|
/** #2073: Present when `includeContent: true` was requested. */
|
|
157
161
|
content?: string;
|
|
@@ -1453,7 +1453,7 @@ export async function bridgeListEntries(options) {
|
|
|
1453
1453
|
// the `status = 'active'` filter matched zero. Treat NULL as
|
|
1454
1454
|
// "legacy-active" — the safe default for any entry that predates the
|
|
1455
1455
|
// status column.
|
|
1456
|
-
const statusFilter = liveMemoryRowSql();
|
|
1456
|
+
const statusFilter = liveMemoryRowSql({ includeExpired: options.includeExpired });
|
|
1457
1457
|
// Count
|
|
1458
1458
|
let total = 0;
|
|
1459
1459
|
try {
|
|
@@ -1468,7 +1468,7 @@ export async function bridgeListEntries(options) {
|
|
|
1468
1468
|
const entries = [];
|
|
1469
1469
|
try {
|
|
1470
1470
|
const stmt = ctx.db.prepare(`
|
|
1471
|
-
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type
|
|
1471
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at
|
|
1472
1472
|
FROM memory_entries
|
|
1473
1473
|
WHERE ${statusFilter} ${extraFilter}
|
|
1474
1474
|
ORDER BY updated_at DESC
|
|
@@ -1486,6 +1486,7 @@ export async function bridgeListEntries(options) {
|
|
|
1486
1486
|
accessCount: row.access_count ?? 0,
|
|
1487
1487
|
createdAt: row.created_at || new Date().toISOString(),
|
|
1488
1488
|
updatedAt: row.updated_at || new Date().toISOString(),
|
|
1489
|
+
expiresAt: row.expires_at ?? null,
|
|
1489
1490
|
hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
|
|
1490
1491
|
provenanceType: row.provenance_type || 'unknown',
|
|
1491
1492
|
};
|
|
@@ -511,6 +511,8 @@ export declare function listEntries(options: {
|
|
|
511
511
|
provenanceFilter?: string[];
|
|
512
512
|
/** ADR-472: include each entry's embedding as int8+scale (`embeddingQ8`); at most MAX_LIST_EMBEDDINGS rows. Read-only. */
|
|
513
513
|
includeEmbedding?: boolean;
|
|
514
|
+
/** #4056: also return rows whose TTL has passed (with `expiresAt`). For cleanup only; normal reads hide them. */
|
|
515
|
+
includeExpired?: boolean;
|
|
514
516
|
}): Promise<{
|
|
515
517
|
success: boolean;
|
|
516
518
|
entries: {
|
|
@@ -521,6 +523,8 @@ export declare function listEntries(options: {
|
|
|
521
523
|
accessCount: number;
|
|
522
524
|
createdAt: string;
|
|
523
525
|
updatedAt: string;
|
|
526
|
+
/** #4056: epoch ms the row expires at; null when it never does. */
|
|
527
|
+
expiresAt?: number | null;
|
|
524
528
|
hasEmbedding: boolean;
|
|
525
529
|
/** #2073: Present when `includeContent: true` was requested. */
|
|
526
530
|
content?: string;
|
|
@@ -6,7 +6,7 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
|
-
import { readFileMaybeEncrypted,
|
|
9
|
+
import { readFileMaybeEncrypted, writeDbImagePreservingFormat, writeFileRestricted } from '../fs-secure.js';
|
|
10
10
|
import { restoreMemoryDbFromBackup } from '../services/memory-backup.js';
|
|
11
11
|
import { validateIdentifier } from '../mcp-tools/validate-input.js';
|
|
12
12
|
/**
|
|
@@ -1248,7 +1248,7 @@ export async function ensureSchemaColumns(dbPath, options = {}) {
|
|
|
1248
1248
|
if (modified) {
|
|
1249
1249
|
// Save updated database
|
|
1250
1250
|
const data = db.export();
|
|
1251
|
-
|
|
1251
|
+
writeDbImagePreservingFormat(dbPath, data, { encrypt: options.encryptWrites ?? true });
|
|
1252
1252
|
}
|
|
1253
1253
|
db.close();
|
|
1254
1254
|
return { success: true, columnsAdded };
|
|
@@ -1975,7 +1975,7 @@ export async function applyTemporalDecay(dbPath) {
|
|
|
1975
1975
|
return await withMemoryDbLock(path_, async () => {
|
|
1976
1976
|
const initSqlJs = (await import('sql.js')).default;
|
|
1977
1977
|
const SQL = await initSqlJs();
|
|
1978
|
-
const fileBuffer =
|
|
1978
|
+
const fileBuffer = readFileMaybeEncrypted(path_, null);
|
|
1979
1979
|
const db = new SQL.Database(fileBuffer);
|
|
1980
1980
|
// Apply decay: confidence *= exp(-decay_rate * days_since_last_use)
|
|
1981
1981
|
const now = Date.now();
|
|
@@ -1992,7 +1992,7 @@ export async function applyTemporalDecay(dbPath) {
|
|
|
1992
1992
|
const changes = db.getRowsModified();
|
|
1993
1993
|
// Save (atomic — issue #2584: a torn full-image flush corrupts the store)
|
|
1994
1994
|
const data = db.export();
|
|
1995
|
-
|
|
1995
|
+
writeDbImagePreservingFormat(path_, data);
|
|
1996
1996
|
db.close();
|
|
1997
1997
|
return {
|
|
1998
1998
|
success: true,
|
|
@@ -2692,7 +2692,7 @@ export async function storeEntry(options) {
|
|
|
2692
2692
|
}
|
|
2693
2693
|
// Save
|
|
2694
2694
|
const data = db.export();
|
|
2695
|
-
|
|
2695
|
+
writeDbImagePreservingFormat(dbPath, data);
|
|
2696
2696
|
db.close();
|
|
2697
2697
|
});
|
|
2698
2698
|
// Add to HNSW index for faster future searches
|
|
@@ -3019,7 +3019,7 @@ export async function listEntries(options) {
|
|
|
3019
3019
|
// that predate the status column may have NULL after migration.
|
|
3020
3020
|
// See memory-bridge.ts:bridgeListEntries for full context.
|
|
3021
3021
|
// Get total count
|
|
3022
|
-
const whereClauses = [liveMemoryRowSql()];
|
|
3022
|
+
const whereClauses = [liveMemoryRowSql({ includeExpired: options.includeExpired })];
|
|
3023
3023
|
const whereParams = [];
|
|
3024
3024
|
if (namespace) {
|
|
3025
3025
|
whereClauses.push('namespace = ?');
|
|
@@ -3044,7 +3044,7 @@ export async function listEntries(options) {
|
|
|
3044
3044
|
const safeLimit = Math.min(parseInt(String(limit), 10) || 100, options.includeEmbedding ? MAX_LIST_EMBEDDINGS : Number.MAX_SAFE_INTEGER);
|
|
3045
3045
|
const safeOffset = parseInt(String(offset), 10) || 0;
|
|
3046
3046
|
// #2120 — same NULL-as-active acceptance as the count above.
|
|
3047
|
-
const listStmt = db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type
|
|
3047
|
+
const listStmt = db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at
|
|
3048
3048
|
FROM memory_entries WHERE ${whereSql}
|
|
3049
3049
|
ORDER BY updated_at DESC LIMIT ? OFFSET ?`);
|
|
3050
3050
|
listStmt.bind([...whereParams, safeLimit, safeOffset]);
|
|
@@ -3057,7 +3057,7 @@ export async function listEntries(options) {
|
|
|
3057
3057
|
const entries = [];
|
|
3058
3058
|
if (result[0]?.values) {
|
|
3059
3059
|
for (const row of result[0].values) {
|
|
3060
|
-
const [id, key, ns, content, embedding, accessCount, createdAt, updatedAt, provenanceTypeVal] = row;
|
|
3060
|
+
const [id, key, ns, content, embedding, accessCount, createdAt, updatedAt, provenanceTypeVal, expiresAtVal] = row;
|
|
3061
3061
|
const entry = {
|
|
3062
3062
|
// #2073: don't truncate id when content is requested — callers
|
|
3063
3063
|
// (notably memory_export) need the full id to round-trip via import.
|
|
@@ -3068,6 +3068,7 @@ export async function listEntries(options) {
|
|
|
3068
3068
|
accessCount: accessCount || 0,
|
|
3069
3069
|
createdAt: createdAt || new Date().toISOString(),
|
|
3070
3070
|
updatedAt: updatedAt || new Date().toISOString(),
|
|
3071
|
+
expiresAt: expiresAtVal ?? null,
|
|
3071
3072
|
hasEmbedding: !!embedding && embedding.length > 10,
|
|
3072
3073
|
provenanceType: provenanceTypeVal || 'unknown'
|
|
3073
3074
|
};
|
|
@@ -3166,7 +3167,7 @@ export async function getEntry(options) {
|
|
|
3166
3167
|
`, [String(id)]);
|
|
3167
3168
|
// Save updated database
|
|
3168
3169
|
const data = db.export();
|
|
3169
|
-
|
|
3170
|
+
writeDbImagePreservingFormat(dbPath, data);
|
|
3170
3171
|
db.close();
|
|
3171
3172
|
let tags = [];
|
|
3172
3173
|
if (tagsJson) {
|
|
@@ -3302,7 +3303,7 @@ export async function deleteEntry(options) {
|
|
|
3302
3303
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
3303
3304
|
// Save updated database
|
|
3304
3305
|
const data = db.export();
|
|
3305
|
-
|
|
3306
|
+
writeDbImagePreservingFormat(dbPath, data, { encrypt: options.encryptWrites ?? true });
|
|
3306
3307
|
db.close();
|
|
3307
3308
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
3308
3309
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
@@ -3472,7 +3473,7 @@ export async function purgeNamespace(options) {
|
|
|
3472
3473
|
const countResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE status = 'active'`);
|
|
3473
3474
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
3474
3475
|
const data = db.export();
|
|
3475
|
-
|
|
3476
|
+
writeDbImagePreservingFormat(dbPath, data, { encrypt: options.encryptWrites ?? true });
|
|
3476
3477
|
db.close();
|
|
3477
3478
|
if (deletedCount > 0 && hnswIndex?.entries) {
|
|
3478
3479
|
for (const [id, entry] of hnswIndex.entries) {
|
|
@@ -69,13 +69,17 @@ export function judgeInstalls(installs, enableEnvSet) {
|
|
|
69
69
|
: '';
|
|
70
70
|
const runs = `${first.path} (${first.version ?? 'version unknown'})`;
|
|
71
71
|
if (!first.version)
|
|
72
|
-
return { status: 'warn', message: `claude resolves to ${runs}${mixed}` };
|
|
72
|
+
return { status: 'warn', message: `claude resolves to ${runs}${mixed}`, fix: `run \`${first.path} --version\` and reinstall Claude Code if it prints nothing` };
|
|
73
73
|
if (!versionLess(first.version, MODS_DEFAULT_ON)) {
|
|
74
|
-
return
|
|
74
|
+
return others.length
|
|
75
|
+
? { status: 'warn', message: `claude resolves to ${runs}: mods on by default${mixed}`, fix: `put the claude you want first on PATH, or remove: ${others.map((i) => i.path).join(' ')}` }
|
|
76
|
+
: { status: 'pass', message: `claude resolves to ${runs}: mods on by default` };
|
|
75
77
|
}
|
|
76
78
|
if (!versionLess(first.version, MODS_FIRST)) {
|
|
77
79
|
return enableEnvSet
|
|
78
|
-
?
|
|
80
|
+
? (others.length
|
|
81
|
+
? { status: 'warn', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)${mixed}`, fix: `put the claude you want first on PATH, or remove: ${others.map((i) => i.path).join(' ')}` }
|
|
82
|
+
: { status: 'pass', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)` })
|
|
79
83
|
: { status: 'warn', message: `claude resolves to ${runs}: below ${MODS_DEFAULT_ON}, mods need CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (not set)${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON}, or export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` };
|
|
80
84
|
}
|
|
81
85
|
return { status: 'warn', message: `claude resolves to ${runs}: predates mods (< ${MODS_FIRST}); classic hooks handle every event${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON} and remove the stale install from PATH` };
|
package/dist/src/mods/install.js
CHANGED
|
@@ -107,18 +107,38 @@ export function installMod(projectRoot, scope, dryRun = false, plugins = MOD_PLU
|
|
|
107
107
|
backup = `${settingsFile}.bak-ruflo-mods-${Date.now()}`;
|
|
108
108
|
copyFileSync(settingsFile, backup);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
// One operation: the settings and the record that lets uninstall take them back are written together.
|
|
111
|
+
// If the record cannot be written, the settings go back as they were (or the file ruflo created goes
|
|
112
|
+
// away), so nothing is left half-installed with no record to remove it by.
|
|
113
|
+
try {
|
|
114
|
+
writeJson(settingsFile, next);
|
|
115
|
+
// What ruflo added once is still ruflo's to remove: merge into the record.
|
|
116
|
+
const record = readRecord(projectRoot) ?? { version: 2, installedAt: '', files: {} };
|
|
117
|
+
const before = record.files[settingsFile] ?? { plugins: [], marketplace: false, env: false };
|
|
118
|
+
record.files[settingsFile] = {
|
|
119
|
+
plugins: [...new Set([...before.plugins, ...added.plugins])],
|
|
120
|
+
marketplace: before.marketplace || added.marketplace,
|
|
121
|
+
env: before.env || added.env,
|
|
122
|
+
claudeInstalled: before.claudeInstalled ?? [],
|
|
123
|
+
};
|
|
124
|
+
record.installedAt = new Date().toISOString();
|
|
125
|
+
writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
try {
|
|
129
|
+
if (backup) {
|
|
130
|
+
copyFileSync(backup, settingsFile);
|
|
131
|
+
unlinkSync(backup);
|
|
132
|
+
}
|
|
133
|
+
else if (existsSync(settingsFile)) {
|
|
134
|
+
unlinkSync(settingsFile);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// the restore itself failed: the original error below is the one to report
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
122
142
|
return { settingsFile, backup, added, dryRun, next };
|
|
123
143
|
}
|
|
124
144
|
/** The install record, v1 (3.50.0: one file, ruflo-mods only) read as v2. */
|
|
@@ -160,7 +160,9 @@ export function resolveFindings(projectRoot, scope, configDir, fs = nodeReadFs,
|
|
|
160
160
|
else if (!market.location) {
|
|
161
161
|
findings.push(installed.installed
|
|
162
162
|
? { name, status: 'pass', message: `installed (${installed.scope} scope, ${installed.installPath})` }
|
|
163
|
-
:
|
|
163
|
+
: plugin.required
|
|
164
|
+
? { name, status: 'warn', message: 'loads once Claude Code has cloned the marketplace (a headless `claude -p` run on a fresh config does not clone it, so the mod is absent there)', fix }
|
|
165
|
+
: { name, status: 'warn', message: 'pending: not released in the ruflo marketplace yet' });
|
|
164
166
|
}
|
|
165
167
|
else if (!plugin.required) {
|
|
166
168
|
findings.push({ name, status: 'warn', message: `pending: not in the ruflo marketplace yet (${market.location})${also}` });
|
package/dist/src/mods/probe.js
CHANGED
|
@@ -8,8 +8,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
8
8
|
import { homedir, platform } from 'node:os';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { findClaudeInstalls, judgeInstalls } from './claude-installs.js';
|
|
11
|
-
import { ENABLE_ENV, MOD_PLUGIN_ID, MOD_PLUGIN_IDS, readRecord, readSettingsFile, settingsFileFor } from './install.js';
|
|
12
|
-
import { claudeConfigDir, resolveFindings } from './plugin-resolve.js';
|
|
11
|
+
import { ENABLE_ENV, MARKETPLACE_NAME, MOD_PLUGIN_ID, MOD_PLUGIN_IDS, readRecord, readSettingsFile, settingsFileFor } from './install.js';
|
|
12
|
+
import { claudeConfigDir, installedState, resolveFindings } from './plugin-resolve.js';
|
|
13
13
|
import { PROJECTION_RELATIVE } from './policy-projection.js';
|
|
14
14
|
export const HANDSHAKE_MARKER = 'RUFLO_MODS_OWNS';
|
|
15
15
|
export const HEARTBEAT_RELATIVE = join('.claude-flow', 'mods', 'session.json');
|
|
@@ -53,6 +53,65 @@ export function legacyCliHooks(files) {
|
|
|
53
53
|
return commands(LEGACY_EVENTS[sub]).some((c) => word.test(c));
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
+
/** Settings files (in the order given) where `enabledPlugins[id]` is exactly `value`. */
|
|
57
|
+
function enabledValues(files, id) {
|
|
58
|
+
const out = new Map();
|
|
59
|
+
for (const f of files) {
|
|
60
|
+
try {
|
|
61
|
+
const v = get(readSettingsFile(f), 'enabledPlugins', id);
|
|
62
|
+
if (v !== undefined)
|
|
63
|
+
out.set(f, v);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// an unparseable file is reported elsewhere; never overwritten
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function partialInstallFindings(root, files, _inputs) {
|
|
72
|
+
const enabled = new Set(MOD_PLUGIN_IDS.filter((id) => [...enabledValues(files, id).values()].includes(true)));
|
|
73
|
+
const out = [];
|
|
74
|
+
const parts = [];
|
|
75
|
+
if (enabled.size > 0 && enabled.size < MOD_PLUGIN_IDS.length) {
|
|
76
|
+
const missing = MOD_PLUGIN_IDS.filter((id) => !enabled.has(id) && ![...enabledValues(files, id).values()].includes(false));
|
|
77
|
+
if (missing.length)
|
|
78
|
+
parts.push(`enabled ${[...enabled].join(', ')} but not ${missing.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
if (enabled.size > 0) {
|
|
81
|
+
const declared = files.some((f) => {
|
|
82
|
+
try {
|
|
83
|
+
return get(readSettingsFile(f), 'extraKnownMarketplaces', MARKETPLACE_NAME) !== undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
if (!declared)
|
|
90
|
+
parts.push(`no \`${MARKETPLACE_NAME}\` marketplace is declared in settings for Claude Code to load them from`);
|
|
91
|
+
}
|
|
92
|
+
const record = readRecord(root);
|
|
93
|
+
if (record) {
|
|
94
|
+
const lost = [];
|
|
95
|
+
for (const [file, added] of Object.entries(record.files)) {
|
|
96
|
+
try {
|
|
97
|
+
const now = readSettingsFile(file);
|
|
98
|
+
for (const id of added.plugins)
|
|
99
|
+
if (get(now, 'enabledPlugins', id) !== true && get(now, 'enabledPlugins', id) !== false)
|
|
100
|
+
lost.push(id);
|
|
101
|
+
if (added.marketplace && get(now, 'extraKnownMarketplaces', MARKETPLACE_NAME) === undefined)
|
|
102
|
+
lost.push(`${MARKETPLACE_NAME} marketplace`);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// unreadable settings: nothing to compare
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (lost.length)
|
|
109
|
+
parts.push(`the install record says ruflo added ${[...new Set(lost)].join(', ')}, which the settings no longer have`);
|
|
110
|
+
}
|
|
111
|
+
if (parts.length)
|
|
112
|
+
out.push({ name: 'partial install', status: 'warn', message: parts.join('; '), fix: 'ruflo mods install' });
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
56
115
|
export function probeMods(inputs) {
|
|
57
116
|
const root = resolve(inputs.projectRoot);
|
|
58
117
|
const home = inputs.home ?? homedir();
|
|
@@ -69,9 +128,38 @@ export function probeMods(inputs) {
|
|
|
69
128
|
return false;
|
|
70
129
|
}
|
|
71
130
|
});
|
|
131
|
+
// A plugin a person set to false is their decision: `ruflo mods install` never flips it, so it is not the repair.
|
|
132
|
+
const disabledIn = enabledIn ? undefined : files.find((f) => {
|
|
133
|
+
try {
|
|
134
|
+
return get(readSettingsFile(f), 'enabledPlugins', MOD_PLUGIN_ID) === false;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const cachedInstall = enabledIn || disabledIn ? { installed: false } : installedState(root, configDir, MOD_PLUGIN_ID, inputs.fs);
|
|
141
|
+
const scopeOf = (f) => (f === files[0] ? 'local' : f === files[1] ? 'project' : 'user');
|
|
72
142
|
findings.push(enabledIn
|
|
73
143
|
? { name: 'ruflo-mods plugin', status: 'pass', message: `enabled in settings: ${enabledIn} (whether Claude Code can load it: see below)` }
|
|
74
|
-
:
|
|
144
|
+
: disabledIn
|
|
145
|
+
? {
|
|
146
|
+
name: 'ruflo-mods plugin',
|
|
147
|
+
status: 'warn',
|
|
148
|
+
message: `disabled in ${disabledIn} (enabledPlugins is false): the mod does not load and classic hooks handle every event; \`ruflo mods install\` leaves a plugin you switched off alone`,
|
|
149
|
+
fix: `claude plugin enable ${MOD_PLUGIN_ID} --scope ${scopeOf(disabledIn)}`,
|
|
150
|
+
}
|
|
151
|
+
: cachedInstall.installed
|
|
152
|
+
? {
|
|
153
|
+
name: 'ruflo-mods plugin',
|
|
154
|
+
status: 'warn',
|
|
155
|
+
message: `installed (${cachedInstall.scope} scope) but not enabled in this project's settings: the mod does not load`,
|
|
156
|
+
fix: `claude plugin enable ${MOD_PLUGIN_ID} --scope ${cachedInstall.scope === 'user' ? 'user' : 'local'}`,
|
|
157
|
+
}
|
|
158
|
+
: { name: 'ruflo-mods plugin', status: 'warn', message: 'not enabled; classic hooks handle every event', fix: 'ruflo mods install' });
|
|
159
|
+
// 1b. A half-finished install: some of the mod plugins enabled and not the rest, a plugin enabled with no
|
|
160
|
+
// marketplace declared for Claude Code to load it from, or keys the install record says ruflo added that
|
|
161
|
+
// the settings file no longer has. `ruflo mods install` is idempotent and adds exactly what is missing.
|
|
162
|
+
findings.push(...partialInstallFindings(root, files, inputs));
|
|
75
163
|
// 1a. Enabled is a request; Claude Code silently skips a plugin its
|
|
76
164
|
// marketplace clone does not carry (a clone older than the plugin).
|
|
77
165
|
if (enabledIn) {
|
|
@@ -163,7 +251,7 @@ export function probeMods(inputs) {
|
|
|
163
251
|
const owned = get(beat, 'owned');
|
|
164
252
|
findings.push(typeof startedAt === 'string'
|
|
165
253
|
? { name: 'last mod start', status: 'pass', message: `${startedAt}, owning ${Array.isArray(owned) && owned.length ? owned.join(', ') : 'nothing'}` }
|
|
166
|
-
: { name: 'last mod start', status: enabledIn ? 'warn' : 'pass', message: 'the mod has not started in this project' + (enabledIn ? ' (restart Claude Code; check function hooks above)' : '') });
|
|
254
|
+
: { name: 'last mod start', status: enabledIn ? 'warn' : 'pass', message: 'the mod has not started in this project' + (enabledIn ? ' (restart Claude Code; check function hooks above)' : ''), ...(enabledIn ? { fix: 'restart Claude Code in this project, then run /ruflo mods' } : {}) });
|
|
167
255
|
if (readRecord(root))
|
|
168
256
|
findings.push({ name: 'install record', status: 'pass', message: join(root, '.claude-flow', 'mods', 'install.json') });
|
|
169
257
|
return findings;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Longest command text kept per row. */
|
|
2
|
+
export declare const MAX_RECORDED_COMMAND_CHARS = 2000;
|
|
3
|
+
/**
|
|
4
|
+
* True when command recording is switched off. Any one of these turns it off:
|
|
5
|
+
* - `CLAUDE_FLOW_HOOKS_RECORD_COMMANDS` = 0 | false | off | no
|
|
6
|
+
* - `CLAUDE_FLOW_HOOKS_ENABLED` = 0 | false | off | no
|
|
7
|
+
* - `hooks.recordCommands: false` in `claude-flow.config.json` or `.claude-flow/config.json`
|
|
8
|
+
*/
|
|
9
|
+
export declare function commandRecordingDisabled(env?: Record<string, string | undefined>, cwd?: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The form of a command that may be written to the store: secrets of the
|
|
12
|
+
* common shapes replaced with `[redacted]`, and the text capped at
|
|
13
|
+
* `MAX_RECORDED_COMMAND_CHARS`. This is best effort, not a guarantee; the off
|
|
14
|
+
* switch is the guarantee.
|
|
15
|
+
*/
|
|
16
|
+
export declare function redactCommandForStorage(command: string): string;
|
|
17
|
+
//# sourceMappingURL=command-recording.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy for what the PostToolUse Bash hook may keep (#4056).
|
|
3
|
+
*
|
|
4
|
+
* `hooks post-command` used to store every command in full as
|
|
5
|
+
* `commands/cmd-<ts>`: 27k rows / 285 MB on one project, full command text
|
|
6
|
+
* that can carry tokens, and no way to switch it off. This module answers two
|
|
7
|
+
* questions for that writer:
|
|
8
|
+
*
|
|
9
|
+
* 1. May a command be recorded at all? (`commandRecordingDisabled`)
|
|
10
|
+
* 2. What form of it may be stored? (`redactCommandForStorage`)
|
|
11
|
+
*
|
|
12
|
+
* Every pattern below is bounded, and the input is capped before any pattern
|
|
13
|
+
* runs, so a hostile command cannot stall the hook (the same lesson as the
|
|
14
|
+
* settings-risk scanner, #4053).
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
/** Longest command text kept per row. */
|
|
19
|
+
export const MAX_RECORDED_COMMAND_CHARS = 2000;
|
|
20
|
+
/** Only this much of a command is ever scanned; the stored text is shorter still. */
|
|
21
|
+
const MAX_SCANNED_CHARS = 8000;
|
|
22
|
+
const OFF_VALUES = new Set(['0', 'false', 'off', 'no']);
|
|
23
|
+
const isOff = (v) => v !== undefined && OFF_VALUES.has(v.trim().toLowerCase());
|
|
24
|
+
function configDisablesRecording(cwd) {
|
|
25
|
+
for (const file of [
|
|
26
|
+
path.resolve(cwd, 'claude-flow.config.json'),
|
|
27
|
+
path.resolve(cwd, '.claude-flow', 'config.json'),
|
|
28
|
+
]) {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(file))
|
|
31
|
+
continue;
|
|
32
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
33
|
+
if (raw?.hooks?.recordCommands === false)
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* malformed config: keep the default (recording on) rather than guess */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* True when command recording is switched off. Any one of these turns it off:
|
|
44
|
+
* - `CLAUDE_FLOW_HOOKS_RECORD_COMMANDS` = 0 | false | off | no
|
|
45
|
+
* - `CLAUDE_FLOW_HOOKS_ENABLED` = 0 | false | off | no
|
|
46
|
+
* - `hooks.recordCommands: false` in `claude-flow.config.json` or `.claude-flow/config.json`
|
|
47
|
+
*/
|
|
48
|
+
export function commandRecordingDisabled(env = process.env, cwd = process.cwd()) {
|
|
49
|
+
if (isOff(env.CLAUDE_FLOW_HOOKS_RECORD_COMMANDS))
|
|
50
|
+
return true;
|
|
51
|
+
if (isOff(env.CLAUDE_FLOW_HOOKS_ENABLED))
|
|
52
|
+
return true;
|
|
53
|
+
return configDisablesRecording(cwd);
|
|
54
|
+
}
|
|
55
|
+
const QUOTED_OR_BARE = `"[^"]{0,512}"|'[^']{0,512}'|[^\\s"']{1,512}`;
|
|
56
|
+
// Order matters little; each rule only ever shortens the text.
|
|
57
|
+
const RULES = [
|
|
58
|
+
// Authorization: Bearer xyz / authorization=Basic xyz
|
|
59
|
+
[/(authorization["']?\s*[:=]\s*)(?:(?:bearer|basic|token)\s+)?[^\s"']{1,512}/gi, '$1[redacted]'],
|
|
60
|
+
// --password x / --token=x / --api-key "x" / -token x
|
|
61
|
+
[new RegExp(`(--?(?:password|passwd|pwd|token|secret|api[-_]?key|access[-_]?token|auth[-_]?token|client[-_]?secret)(?:=|\\s+))(?:${QUOTED_OR_BARE})`, 'gi'), '$1[redacted]'],
|
|
62
|
+
// NPM_TOKEN=x AWS_SECRET_ACCESS_KEY=x DB_PASSWORD=x
|
|
63
|
+
[new RegExp(`\\b([A-Za-z_][A-Za-z0-9_]{0,63}(?:TOKEN|SECRET|PASSWORD|PASSWD|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIALS?))=(?:${QUOTED_OR_BARE})`, 'gi'), '$1=[redacted]'],
|
|
64
|
+
// user:password@host in a URL
|
|
65
|
+
[/(:\/\/[^\s:@/]{1,128}:)[^\s@/]{1,256}(@)/g, '$1[redacted]$2'],
|
|
66
|
+
// well-known token shapes
|
|
67
|
+
[/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|npm_[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})/g, '[redacted]'],
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* The form of a command that may be written to the store: secrets of the
|
|
71
|
+
* common shapes replaced with `[redacted]`, and the text capped at
|
|
72
|
+
* `MAX_RECORDED_COMMAND_CHARS`. This is best effort, not a guarantee; the off
|
|
73
|
+
* switch is the guarantee.
|
|
74
|
+
*/
|
|
75
|
+
export function redactCommandForStorage(command) {
|
|
76
|
+
let text = String(command);
|
|
77
|
+
if (text.length > MAX_SCANNED_CHARS) {
|
|
78
|
+
// Cut at a token boundary so a half-token at the cut is not left behind.
|
|
79
|
+
const cut = text.slice(0, MAX_SCANNED_CHARS);
|
|
80
|
+
const lastSpace = cut.search(/\s\S*$/);
|
|
81
|
+
text = lastSpace > 0 ? cut.slice(0, lastSpace) : cut;
|
|
82
|
+
text += ' ';
|
|
83
|
+
}
|
|
84
|
+
for (const [re, replacement] of RULES)
|
|
85
|
+
text = text.replace(re, replacement);
|
|
86
|
+
if (text.length > MAX_RECORDED_COMMAND_CHARS) {
|
|
87
|
+
text = `${text.slice(0, MAX_RECORDED_COMMAND_CHARS)} …[truncated]`;
|
|
88
|
+
}
|
|
89
|
+
return text.trimEnd();
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=command-recording.js.map
|
|
@@ -14,10 +14,28 @@
|
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
16
16
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
17
|
-
import { homedir } from 'node:os';
|
|
17
|
+
import { homedir, userInfo } from 'node:os';
|
|
18
18
|
const LOG_FILE = 'ledger-anchors.json';
|
|
19
19
|
const MIRROR_FILE = 'ledger-anchor-head.json';
|
|
20
20
|
export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', LOG_FILE);
|
|
21
|
+
/**
|
|
22
|
+
* The home directory trust material lives under: $HOME when it is an absolute path, else the account's home. An empty or relative
|
|
23
|
+
* $HOME (an unset variable in a wrapper script, `HOME=. cmd`) would make join() yield a path relative to the working directory and
|
|
24
|
+
* drop the HMAC key and anchor mirror into the project; that is never wanted, so it falls back to the account home or refuses.
|
|
25
|
+
*/
|
|
26
|
+
function trustHome() {
|
|
27
|
+
const home = homedir();
|
|
28
|
+
if (home && isAbsolute(home))
|
|
29
|
+
return home;
|
|
30
|
+
let account = '';
|
|
31
|
+
try {
|
|
32
|
+
account = userInfo().homedir;
|
|
33
|
+
}
|
|
34
|
+
catch { /* no account entry for this uid */ }
|
|
35
|
+
if (account && isAbsolute(account))
|
|
36
|
+
return account;
|
|
37
|
+
throw new Error('policy-trust-home-unresolved: $HOME is empty or relative and this account has no home directory; set HOME to an absolute path');
|
|
38
|
+
}
|
|
21
39
|
/**
|
|
22
40
|
* Directory holding per-project trust material (anchor mirror, HMAC key).
|
|
23
41
|
* Resolved from the environment at call time (#3919): $XDG_CONFIG_HOME when it
|
|
@@ -26,7 +44,7 @@ export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claud
|
|
|
26
44
|
* user who already has trust state under the legacy ~/.config keeps using it.
|
|
27
45
|
*/
|
|
28
46
|
export function policyTrustRoot() {
|
|
29
|
-
const legacy = join(
|
|
47
|
+
const legacy = join(trustHome(), '.config', 'ruflo', 'policy-trust');
|
|
30
48
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
31
49
|
if (!xdg || !isAbsolute(xdg))
|
|
32
50
|
return legacy;
|
|
@@ -751,19 +751,36 @@ enabled = true
|
|
|
751
751
|
|
|
752
752
|
### .codex/config.toml (Local Overrides)
|
|
753
753
|
|
|
754
|
+
`init` creates this gitignored file with **standard** trust by default:
|
|
755
|
+
|
|
754
756
|
```toml
|
|
755
757
|
# Local development overrides (gitignored)
|
|
756
758
|
# These settings override .agents/config.toml
|
|
757
759
|
|
|
758
|
-
approval_policy = "
|
|
759
|
-
sandbox_mode = "
|
|
760
|
-
web_search = "
|
|
760
|
+
approval_policy = "on-request"
|
|
761
|
+
sandbox_mode = "workspace-write"
|
|
762
|
+
web_search = "cached"
|
|
761
763
|
|
|
762
764
|
# Disable MCP in local if needed
|
|
763
765
|
[mcp_servers.claude-flow]
|
|
764
766
|
enabled = false
|
|
765
767
|
```
|
|
766
768
|
|
|
769
|
+
Codex can then write inside the workspace only and asks before anything riskier.
|
|
770
|
+
Opt in to the old permissive file (`approval_policy = "never"`,
|
|
771
|
+
`sandbox_mode = "danger-full-access"`, `web_search = "live"`) explicitly:
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
npx ruflo@latest init --dual --codex-trust full # or: RUFLO_CODEX_TRUST=full
|
|
775
|
+
npx @claude-flow/codex init --codex-trust full
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
`init` prints which level you got. An existing `.codex/config.toml` is never
|
|
779
|
+
rewritten, not even with `--force` (it holds your own sandbox and approval
|
|
780
|
+
choices), so to change the level of an existing file edit those three keys.
|
|
781
|
+
Unknown `--codex-trust` values are rejected on the command line and fall back to
|
|
782
|
+
`standard` (with a warning) from the environment variable.
|
|
783
|
+
|
|
767
784
|
### Environment Variables
|
|
768
785
|
|
|
769
786
|
```bash
|
|
@@ -75,6 +75,7 @@ program
|
|
|
75
75
|
.option('-s, --skills <skills>', 'Comma-separated list of skills to include')
|
|
76
76
|
.option('-f, --force', 'Overwrite existing files', false)
|
|
77
77
|
.option('--dual', 'Generate both Codex and Claude Code configurations', false)
|
|
78
|
+
.option('--codex-trust <level>', 'Trust level of the generated .codex/config.toml: standard (default: workspace-write sandbox, on-request approvals) or full (danger-full-access, no approvals)')
|
|
78
79
|
.option('-p, --path <path>', 'Project path', process.cwd())
|
|
79
80
|
.option('-q, --quiet', 'Suppress verbose output', false)
|
|
80
81
|
.action(async (options) => {
|
|
@@ -82,6 +83,11 @@ program
|
|
|
82
83
|
if (!options.quiet) {
|
|
83
84
|
printBanner();
|
|
84
85
|
}
|
|
86
|
+
if (options.codexTrust !== undefined && !['standard', 'full'].includes(options.codexTrust)) {
|
|
87
|
+
console.error(chalk.red(`Invalid --codex-trust: ${options.codexTrust}`));
|
|
88
|
+
console.log(chalk.gray('Valid levels: standard, full'));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
85
91
|
// Validate template
|
|
86
92
|
const validTemplates = ['minimal', 'default', 'full', 'enterprise'];
|
|
87
93
|
if (!validTemplates.includes(options.template)) {
|
|
@@ -120,6 +126,7 @@ program
|
|
|
120
126
|
skills,
|
|
121
127
|
force: options.force,
|
|
122
128
|
dual: options.dual,
|
|
129
|
+
codexTrust: options.codexTrust,
|
|
123
130
|
});
|
|
124
131
|
if (result.success) {
|
|
125
132
|
console.log(chalk.green.bold('\n Project initialized successfully!'));
|