iranti 0.3.11 → 0.3.13
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 +33 -5
- package/dist/scripts/claude-code-memory-hook.js +3 -1
- package/dist/scripts/codex-setup.js +150 -0
- package/dist/scripts/iranti-cli.js +311 -28
- package/dist/scripts/iranti-mcp.js +26 -0
- package/dist/src/api/middleware/rateLimit.d.ts +15 -14
- package/dist/src/api/middleware/rateLimit.d.ts.map +1 -1
- package/dist/src/api/middleware/rateLimit.js +110 -25
- package/dist/src/api/middleware/rateLimit.js.map +1 -1
- package/dist/src/api/server.js +10 -1
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +2 -0
- package/dist/src/attendant/AttendantInstance.js.map +1 -1
- package/dist/src/lib/hostMemoryFormatting.js +1 -1
- package/dist/src/lib/hostMemoryFormatting.js.map +1 -1
- package/dist/src/lib/protocolEnforcement.d.ts +7 -0
- package/dist/src/lib/protocolEnforcement.d.ts.map +1 -1
- package/dist/src/lib/protocolEnforcement.js +15 -0
- package/dist/src/lib/protocolEnforcement.js.map +1 -1
- package/dist/src/sdk/index.d.ts +7 -0
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +47 -0
- package/dist/src/sdk/index.js.map +1 -1
- package/dist/src/security/apiKeys.d.ts.map +1 -1
- package/dist/src/security/apiKeys.js +18 -3
- package/dist/src/security/apiKeys.js.map +1 -1
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# Iranti
|
|
2
2
|
|
|
3
3
|
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
|
4
|
+
[](https://modelcontextprotocol.io)
|
|
5
|
+
[](https://www.npmjs.com/package/iranti)
|
|
4
6
|
[](https://www.python.org/downloads/)
|
|
5
7
|
[](https://www.typescriptlang.org/)
|
|
6
8
|
[](https://www.crewai.com/)
|
|
7
9
|
|
|
8
|
-
**Memory infrastructure for multi-agent AI systems.**
|
|
10
|
+
**Memory infrastructure for multi-agent AI systems — with a built-in MCP server.**
|
|
9
11
|
|
|
10
12
|
Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup. Iranti also supports hybrid search (lexical + vector) when exact keys are unknown. Memory persists across sessions and survives context window limits.
|
|
11
13
|
|
|
12
|
-
**Repo version:** `0.3.
|
|
14
|
+
**Repo version:** `0.3.11`
|
|
13
15
|
Published packages:
|
|
14
|
-
- npm `iranti@0.3.
|
|
15
|
-
- npm `@iranti/sdk@0.3.
|
|
16
|
-
- PyPI `iranti==0.3.
|
|
16
|
+
- npm `iranti@0.3.11`
|
|
17
|
+
- npm `@iranti/sdk@0.3.11`
|
|
18
|
+
- PyPI `iranti==0.3.11`
|
|
17
19
|
|
|
18
20
|
---
|
|
19
21
|
|
|
@@ -23,6 +25,32 @@ Iranti is a knowledge base for multi-agent systems. The primary read path is ide
|
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
28
|
+
## MCP Server
|
|
29
|
+
|
|
30
|
+
Iranti ships a stdio MCP server compatible with Claude Code, GitHub Copilot, Codex, and any MCP-compliant client:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Fast path for Claude Code
|
|
34
|
+
iranti claude-setup
|
|
35
|
+
|
|
36
|
+
# Fast path for Codex
|
|
37
|
+
iranti codex-setup
|
|
38
|
+
|
|
39
|
+
# Fast path for GitHub Copilot
|
|
40
|
+
iranti copilot-setup
|
|
41
|
+
|
|
42
|
+
# Manual / other MCP clients
|
|
43
|
+
iranti mcp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
MCP tools exposed: `iranti_handshake`, `iranti_attend`, `iranti_write`, `iranti_query`, `iranti_search`, `iranti_checkpoint`, `iranti_ingest`, `iranti_relate`, `iranti_related`, `iranti_related_deep`, `iranti_history`, `iranti_who_knows`, `iranti_observe`, and more.
|
|
47
|
+
|
|
48
|
+
Full setup guides:
|
|
49
|
+
- [Claude Code](docs/guides/claude-code.md)
|
|
50
|
+
- [Codex](docs/guides/codex.md)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
26
54
|
## Runtime Roles
|
|
27
55
|
|
|
28
56
|
- **User**: Person who interacts with an app or chatbot built on Iranti.
|
|
@@ -315,7 +315,9 @@ function emitHookContext(event, additionalContext) {
|
|
|
315
315
|
additionalContext,
|
|
316
316
|
},
|
|
317
317
|
};
|
|
318
|
-
|
|
318
|
+
// Use synchronous write to fd 1 to avoid libuv UV_HANDLE_CLOSING assertion
|
|
319
|
+
// on Windows when Node exits while an async stdout write is still pending.
|
|
320
|
+
require('fs').writeSync(1, `${JSON.stringify(payload)}\n`);
|
|
319
321
|
}
|
|
320
322
|
function shouldFetchMemory(prompt) {
|
|
321
323
|
const normalized = prompt.trim();
|
|
@@ -364,6 +364,147 @@ function writeWorkspaceVsCodeMcpFile(projectEnv, options) {
|
|
|
364
364
|
}, null, 2)}\n`, 'utf8');
|
|
365
365
|
return { filePath: mcpFile, status: 'updated' };
|
|
366
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Protocol reminder hook script content for Codex UserPromptSubmit.
|
|
369
|
+
* Same content as the Claude Code version — fires before every user prompt.
|
|
370
|
+
*/
|
|
371
|
+
function buildProtocolReminderHookScript() {
|
|
372
|
+
return [
|
|
373
|
+
'#!/usr/bin/env node',
|
|
374
|
+
"'use strict';",
|
|
375
|
+
'// Iranti protocol reminder hook — fires on UserPromptSubmit for any Iranti project.',
|
|
376
|
+
'// Cross-platform: runs on Windows, macOS, Linux via Node.js.',
|
|
377
|
+
'// Exits cleanly with no output for non-Iranti projects.',
|
|
378
|
+
"const fs = require('fs');",
|
|
379
|
+
"const path = require('path');",
|
|
380
|
+
'',
|
|
381
|
+
"const envFile = path.join(process.cwd(), '.env.iranti');",
|
|
382
|
+
'if (!fs.existsSync(envFile)) process.exit(0);',
|
|
383
|
+
'',
|
|
384
|
+
'const content = [',
|
|
385
|
+
" 'IRANTI PROTOCOL (required this turn):',",
|
|
386
|
+
" '1. iranti_attend(phase=pre-response) BEFORE replying',",
|
|
387
|
+
" '2. iranti_attend BEFORE each Read / Grep / Glob / Bash / WebSearch / WebFetch',",
|
|
388
|
+
" '3. iranti_write AFTER each Edit or Write:',",
|
|
389
|
+
" ' entity: project/[id]/file/[filename] -- not the broad project entity',",
|
|
390
|
+
" ' value must include: absolutePath, lines, before, after, verify, why',",
|
|
391
|
+
" '4. iranti_write AFTER each Bash that reveals system state (build, errors, ports, env)',",
|
|
392
|
+
" '5. iranti_write AFTER each WebSearch/WebFetch -- write findings AND dead ends / 404s',",
|
|
393
|
+
" '6. iranti_attend(phase=post-response) AFTER every response without exception',",
|
|
394
|
+
"].join('\\n') + '\\n';",
|
|
395
|
+
"require('fs').writeSync(1, content);",
|
|
396
|
+
'',
|
|
397
|
+
].join('\n');
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Write the protocol-reminder hook script into the project's .codex/ directory
|
|
401
|
+
* and return a status result matching the workspace file pattern.
|
|
402
|
+
*/
|
|
403
|
+
function writeProtocolReminderHook(projectEnv) {
|
|
404
|
+
const projectPath = node_path_1.default.dirname(projectEnv);
|
|
405
|
+
const codexDir = node_path_1.default.join(projectPath, '.codex');
|
|
406
|
+
const hookFile = node_path_1.default.join(codexDir, 'iranti-protocol-hook.js');
|
|
407
|
+
const hookContent = buildProtocolReminderHookScript();
|
|
408
|
+
node_fs_1.default.mkdirSync(codexDir, { recursive: true });
|
|
409
|
+
if (!node_fs_1.default.existsSync(hookFile)) {
|
|
410
|
+
node_fs_1.default.writeFileSync(hookFile, hookContent, 'utf8');
|
|
411
|
+
return { filePath: hookFile, status: 'created' };
|
|
412
|
+
}
|
|
413
|
+
const existing = node_fs_1.default.readFileSync(hookFile, 'utf8');
|
|
414
|
+
if (existing !== hookContent) {
|
|
415
|
+
node_fs_1.default.writeFileSync(hookFile, hookContent, 'utf8');
|
|
416
|
+
return { filePath: hookFile, status: 'updated' };
|
|
417
|
+
}
|
|
418
|
+
return { filePath: hookFile, status: 'unchanged' };
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Write a .codex/hooks.json referencing the protocol-reminder hook.
|
|
422
|
+
* This fires on UserPromptSubmit when the codex_hooks feature is enabled.
|
|
423
|
+
*/
|
|
424
|
+
function writeCodexHooksConfig(projectEnv) {
|
|
425
|
+
const projectPath = node_path_1.default.dirname(projectEnv);
|
|
426
|
+
const codexDir = node_path_1.default.join(projectPath, '.codex');
|
|
427
|
+
const hooksConfigFile = node_path_1.default.join(codexDir, 'hooks.json');
|
|
428
|
+
const hookScriptPath = node_path_1.default.join(codexDir, 'iranti-protocol-hook.js');
|
|
429
|
+
const hooksConfig = {
|
|
430
|
+
hooks: {
|
|
431
|
+
UserPromptSubmit: [
|
|
432
|
+
{
|
|
433
|
+
matcher: '',
|
|
434
|
+
hooks: [
|
|
435
|
+
{
|
|
436
|
+
type: 'command',
|
|
437
|
+
command: `node ${hookScriptPath.replace(/\\/g, '/')}`,
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
node_fs_1.default.mkdirSync(codexDir, { recursive: true });
|
|
445
|
+
const nextContent = `${JSON.stringify(hooksConfig, null, 2)}\n`;
|
|
446
|
+
if (!node_fs_1.default.existsSync(hooksConfigFile)) {
|
|
447
|
+
node_fs_1.default.writeFileSync(hooksConfigFile, nextContent, 'utf8');
|
|
448
|
+
return { filePath: hooksConfigFile, status: 'created' };
|
|
449
|
+
}
|
|
450
|
+
const existing = node_fs_1.default.readFileSync(hooksConfigFile, 'utf8');
|
|
451
|
+
if (existing === nextContent) {
|
|
452
|
+
return { filePath: hooksConfigFile, status: 'unchanged' };
|
|
453
|
+
}
|
|
454
|
+
// Merge: keep existing hooks, add/replace UserPromptSubmit from iranti.
|
|
455
|
+
try {
|
|
456
|
+
const parsed = JSON.parse(existing);
|
|
457
|
+
const existingHooks = parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)
|
|
458
|
+
? parsed.hooks
|
|
459
|
+
: {};
|
|
460
|
+
const merged = {
|
|
461
|
+
...parsed,
|
|
462
|
+
hooks: {
|
|
463
|
+
...existingHooks,
|
|
464
|
+
UserPromptSubmit: hooksConfig.hooks.UserPromptSubmit,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
const mergedContent = `${JSON.stringify(merged, null, 2)}\n`;
|
|
468
|
+
if (mergedContent === existing) {
|
|
469
|
+
return { filePath: hooksConfigFile, status: 'unchanged' };
|
|
470
|
+
}
|
|
471
|
+
node_fs_1.default.writeFileSync(hooksConfigFile, mergedContent, 'utf8');
|
|
472
|
+
return { filePath: hooksConfigFile, status: 'updated' };
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// Can't parse existing — overwrite
|
|
476
|
+
node_fs_1.default.writeFileSync(hooksConfigFile, nextContent, 'utf8');
|
|
477
|
+
return { filePath: hooksConfigFile, status: 'updated' };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Check if the codex_hooks feature is enabled globally.
|
|
482
|
+
*/
|
|
483
|
+
function isCodexHooksFeatureEnabled(repoRoot) {
|
|
484
|
+
try {
|
|
485
|
+
const output = run('codex', ['features', 'list'], repoRoot);
|
|
486
|
+
const match = output.match(/codex_hooks\s+\S+\s+(true|false)/);
|
|
487
|
+
return match?.[1] === 'true';
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Enable the codex_hooks feature flag if not already enabled.
|
|
495
|
+
*/
|
|
496
|
+
function ensureCodexHooksFeature(repoRoot) {
|
|
497
|
+
if (isCodexHooksFeatureEnabled(repoRoot)) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
run('codex', ['features', 'enable', 'codex_hooks'], repoRoot);
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
367
508
|
function canUseInstalledIranti(repoRoot) {
|
|
368
509
|
try {
|
|
369
510
|
run('iranti', ['mcp', '--help'], repoRoot);
|
|
@@ -428,8 +569,12 @@ async function main() {
|
|
|
428
569
|
vscode: writeWorkspaceVsCodeMcpFile(workspaceProjectEnv, options),
|
|
429
570
|
agents: writeWorkspaceAgentsFile(workspaceProjectEnv),
|
|
430
571
|
irantiMd: writeWorkspaceIrantiMdFile(workspaceProjectEnv),
|
|
572
|
+
protocolHook: writeProtocolReminderHook(workspaceProjectEnv),
|
|
573
|
+
hooksConfig: writeCodexHooksConfig(workspaceProjectEnv),
|
|
431
574
|
}
|
|
432
575
|
: null;
|
|
576
|
+
// Enable the codex_hooks feature flag so the UserPromptSubmit hook fires.
|
|
577
|
+
const hooksFeatureEnabled = ensureCodexHooksFeature(repoRoot);
|
|
433
578
|
const registered = run('codex', ['mcp', 'get', options.name], repoRoot);
|
|
434
579
|
console.log(registered);
|
|
435
580
|
console.log('');
|
|
@@ -468,6 +613,9 @@ async function main() {
|
|
|
468
613
|
console.log(`Workspace .vscode/mcp.json: ${workspaceFilesResult.vscode.status} (${workspaceFilesResult.vscode.filePath})`);
|
|
469
614
|
console.log(`Workspace AGENTS.md: ${workspaceFilesResult.agents.status} (${workspaceFilesResult.agents.filePath})`);
|
|
470
615
|
console.log(`Workspace IRANTI.md: ${workspaceFilesResult.irantiMd.status} (${workspaceFilesResult.irantiMd.filePath})`);
|
|
616
|
+
console.log(`Protocol hook: ${workspaceFilesResult.protocolHook.status} (${workspaceFilesResult.protocolHook.filePath})`);
|
|
617
|
+
console.log(`Hooks config: ${workspaceFilesResult.hooksConfig.status} (${workspaceFilesResult.hooksConfig.filePath})`);
|
|
618
|
+
console.log(`codex_hooks feature: ${hooksFeatureEnabled ? 'enabled' : 'not enabled (UserPromptSubmit hook requires codex_hooks feature)'}`);
|
|
471
619
|
const closeout = await (0, scaffoldCloseout_1.writeProjectScaffoldCloseout)({
|
|
472
620
|
tool: 'codex',
|
|
473
621
|
projectPath: node_path_1.default.dirname(boundProjectEnv),
|
|
@@ -477,6 +625,8 @@ async function main() {
|
|
|
477
625
|
{ path: workspaceFilesResult.vscode.filePath, status: workspaceFilesResult.vscode.status },
|
|
478
626
|
{ path: workspaceFilesResult.agents.filePath, status: workspaceFilesResult.agents.status },
|
|
479
627
|
{ path: workspaceFilesResult.irantiMd.filePath, status: workspaceFilesResult.irantiMd.status },
|
|
628
|
+
{ path: workspaceFilesResult.protocolHook.filePath, status: workspaceFilesResult.protocolHook.status },
|
|
629
|
+
{ path: workspaceFilesResult.hooksConfig.filePath, status: workspaceFilesResult.hooksConfig.status },
|
|
480
630
|
],
|
|
481
631
|
agentId: options.agent || 'codex_code',
|
|
482
632
|
});
|
|
@@ -2231,6 +2231,218 @@ function makeClaudeHookEntry(event, projectEnvPath) {
|
|
|
2231
2231
|
],
|
|
2232
2232
|
};
|
|
2233
2233
|
}
|
|
2234
|
+
function makeClaudeWriteGuardHookEntry(hookScriptDir) {
|
|
2235
|
+
return {
|
|
2236
|
+
matcher: 'Edit|Write',
|
|
2237
|
+
hooks: [
|
|
2238
|
+
{
|
|
2239
|
+
type: 'command',
|
|
2240
|
+
command: `node ${quoteClaudeHookArg(path_1.default.join(hookScriptDir, 'iranti-write-guard-hook.js'))}`,
|
|
2241
|
+
},
|
|
2242
|
+
],
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
function makeClaudeEditTrackerHookEntry(hookScriptDir) {
|
|
2246
|
+
return {
|
|
2247
|
+
matcher: 'Edit|Write',
|
|
2248
|
+
hooks: [
|
|
2249
|
+
{
|
|
2250
|
+
type: 'command',
|
|
2251
|
+
command: `node ${quoteClaudeHookArg(path_1.default.join(hookScriptDir, 'iranti-edit-tracker-hook.js'))}`,
|
|
2252
|
+
},
|
|
2253
|
+
],
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Embedded JS content for the write-guard hook (PreToolUse).
|
|
2258
|
+
* Blocks Edit/Write if the agent has pending unwritten edits in .iranti-write-debt.
|
|
2259
|
+
*/
|
|
2260
|
+
function buildWriteGuardHookScript() {
|
|
2261
|
+
return `#!/usr/bin/env node
|
|
2262
|
+
'use strict';
|
|
2263
|
+
// Iranti write-guard hook — fires on PreToolUse for Edit/Write.
|
|
2264
|
+
// Checks whether the agent has pending unwritten edits by querying the
|
|
2265
|
+
// .iranti-write-debt signal file. If the agent edited files without calling
|
|
2266
|
+
// iranti_write, this hook DENIES the next edit and injects a reminder.
|
|
2267
|
+
//
|
|
2268
|
+
// Falls through silently (allows) for non-Iranti projects or if check fails.
|
|
2269
|
+
const fs = require('fs');
|
|
2270
|
+
const path = require('path');
|
|
2271
|
+
|
|
2272
|
+
const envFile = path.join(process.cwd(), '.env.iranti');
|
|
2273
|
+
if (!fs.existsSync(envFile)) {
|
|
2274
|
+
process.exit(0);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
let input = '';
|
|
2278
|
+
try {
|
|
2279
|
+
input = fs.readFileSync(0, 'utf8');
|
|
2280
|
+
} catch {
|
|
2281
|
+
process.exit(0);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
let hookData;
|
|
2285
|
+
try {
|
|
2286
|
+
hookData = JSON.parse(input);
|
|
2287
|
+
} catch {
|
|
2288
|
+
process.exit(0);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
const toolName = hookData.tool_name || hookData.toolName || '';
|
|
2292
|
+
if (toolName !== 'Edit' && toolName !== 'Write') {
|
|
2293
|
+
process.exit(0);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
const signalFile = path.join(process.cwd(), '.iranti-write-debt');
|
|
2297
|
+
if (!fs.existsSync(signalFile)) {
|
|
2298
|
+
process.exit(0);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
let debt;
|
|
2302
|
+
try {
|
|
2303
|
+
debt = JSON.parse(fs.readFileSync(signalFile, 'utf8'));
|
|
2304
|
+
} catch {
|
|
2305
|
+
process.exit(0);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const pendingEdits = debt.pendingEdits || 0;
|
|
2309
|
+
const lastEditAt = debt.lastEditAt || null;
|
|
2310
|
+
|
|
2311
|
+
if (pendingEdits < 1) {
|
|
2312
|
+
process.exit(0);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
const output = JSON.stringify({
|
|
2316
|
+
hookEventName: 'PreToolUse',
|
|
2317
|
+
permissionDecision: 'deny',
|
|
2318
|
+
permissionDecisionReason: \`Iranti write-guard: \${pendingEdits} file edit(s) since last iranti_write (last edit: \${lastEditAt || 'unknown'}). Call iranti_write for each pending edit before making new changes.\`,
|
|
2319
|
+
additionalContext: [
|
|
2320
|
+
'IRANTI WRITE-GUARD BLOCKED THIS EDIT.',
|
|
2321
|
+
\`You have \${pendingEdits} pending file edit(s) that have not been written to Iranti shared memory.\`,
|
|
2322
|
+
'Before making any more edits, you MUST call iranti_write for each pending edit.',
|
|
2323
|
+
'Include: entity (project/[id]/file/[filename]), absolutePath, lines changed, what changed and why.',
|
|
2324
|
+
'After writing all pending edits, this guard will allow new edits.',
|
|
2325
|
+
].join('\\n'),
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
require('fs').writeSync(1, output);
|
|
2329
|
+
`;
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Embedded JS content for the edit-tracker hook (PostToolUse).
|
|
2333
|
+
* Increments the .iranti-write-debt counter after each Edit/Write.
|
|
2334
|
+
*/
|
|
2335
|
+
function buildEditTrackerHookScript() {
|
|
2336
|
+
return `#!/usr/bin/env node
|
|
2337
|
+
'use strict';
|
|
2338
|
+
// Iranti edit-tracker hook — fires on PostToolUse for Edit/Write.
|
|
2339
|
+
// Increments the write-debt counter so the PreToolUse write-guard knows
|
|
2340
|
+
// the agent has pending unwritten edits.
|
|
2341
|
+
//
|
|
2342
|
+
// The write-debt file (.iranti-write-debt) is cleared by the MCP server
|
|
2343
|
+
// when iranti_write is called, completing the enforcement loop.
|
|
2344
|
+
//
|
|
2345
|
+
// Falls through silently for non-Iranti projects.
|
|
2346
|
+
const fs = require('fs');
|
|
2347
|
+
const path = require('path');
|
|
2348
|
+
|
|
2349
|
+
const envFile = path.join(process.cwd(), '.env.iranti');
|
|
2350
|
+
if (!fs.existsSync(envFile)) {
|
|
2351
|
+
process.exit(0);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
let input = '';
|
|
2355
|
+
try {
|
|
2356
|
+
input = fs.readFileSync(0, 'utf8');
|
|
2357
|
+
} catch {
|
|
2358
|
+
process.exit(0);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
let hookData;
|
|
2362
|
+
try {
|
|
2363
|
+
hookData = JSON.parse(input);
|
|
2364
|
+
} catch {
|
|
2365
|
+
process.exit(0);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
const toolName = hookData.tool_name || hookData.toolName || '';
|
|
2369
|
+
if (toolName !== 'Edit' && toolName !== 'Write') {
|
|
2370
|
+
process.exit(0);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const signalFile = path.join(process.cwd(), '.iranti-write-debt');
|
|
2374
|
+
let debt = { pendingEdits: 0, edits: [] };
|
|
2375
|
+
|
|
2376
|
+
try {
|
|
2377
|
+
if (fs.existsSync(signalFile)) {
|
|
2378
|
+
debt = JSON.parse(fs.readFileSync(signalFile, 'utf8'));
|
|
2379
|
+
}
|
|
2380
|
+
} catch {
|
|
2381
|
+
debt = { pendingEdits: 0, edits: [] };
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
const editedFile = hookData.tool_input?.file_path
|
|
2385
|
+
|| hookData.input?.file_path
|
|
2386
|
+
|| hookData.tool_input?.filePath
|
|
2387
|
+
|| hookData.input?.filePath
|
|
2388
|
+
|| 'unknown';
|
|
2389
|
+
|
|
2390
|
+
debt.pendingEdits = (debt.pendingEdits || 0) + 1;
|
|
2391
|
+
debt.lastEditAt = new Date().toISOString();
|
|
2392
|
+
debt.edits = debt.edits || [];
|
|
2393
|
+
debt.edits.push({
|
|
2394
|
+
file: editedFile,
|
|
2395
|
+
at: debt.lastEditAt,
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
if (debt.edits.length > 20) {
|
|
2399
|
+
debt.edits = debt.edits.slice(-20);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
try {
|
|
2403
|
+
fs.writeFileSync(signalFile, JSON.stringify(debt, null, 2), 'utf8');
|
|
2404
|
+
} catch {
|
|
2405
|
+
// Can't write signal — that's okay, guard just won't fire
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const output = JSON.stringify({
|
|
2409
|
+
hookEventName: 'PostToolUse',
|
|
2410
|
+
additionalContext: \`Iranti: file edited (\${path.basename(editedFile)}). \${debt.pendingEdits} pending edit(s) awaiting iranti_write. Write before making more edits.\`,
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
require('fs').writeSync(1, output);
|
|
2414
|
+
`;
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Embedded JS content for the Codex/generic protocol reminder hook (UserPromptSubmit).
|
|
2418
|
+
* Injects a protocol reminder before every user prompt in Codex sessions.
|
|
2419
|
+
*/
|
|
2420
|
+
function buildProtocolReminderHookScript() {
|
|
2421
|
+
return `#!/usr/bin/env node
|
|
2422
|
+
'use strict';
|
|
2423
|
+
// Iranti protocol reminder hook — fires on UserPromptSubmit for any Iranti project.
|
|
2424
|
+
// Cross-platform: runs on Windows, macOS, Linux via Node.js.
|
|
2425
|
+
// Exits cleanly with no output for non-Iranti projects.
|
|
2426
|
+
const fs = require('fs');
|
|
2427
|
+
const path = require('path');
|
|
2428
|
+
|
|
2429
|
+
const envFile = path.join(process.cwd(), '.env.iranti');
|
|
2430
|
+
if (!fs.existsSync(envFile)) process.exit(0);
|
|
2431
|
+
|
|
2432
|
+
const content = [
|
|
2433
|
+
'IRANTI PROTOCOL (required this turn):',
|
|
2434
|
+
'1. iranti_attend(phase=pre-response) BEFORE replying',
|
|
2435
|
+
'2. iranti_attend BEFORE each Read / Grep / Glob / Bash / WebSearch / WebFetch',
|
|
2436
|
+
'3. iranti_write AFTER each Edit or Write:',
|
|
2437
|
+
' entity: project/[id]/file/[filename] -- not the broad project entity',
|
|
2438
|
+
' value must include: absolutePath, lines, before, after, verify, why',
|
|
2439
|
+
'4. iranti_write AFTER each Bash that reveals system state (build, errors, ports, env)',
|
|
2440
|
+
'5. iranti_write AFTER each WebSearch/WebFetch -- write findings AND dead ends / 404s',
|
|
2441
|
+
'6. iranti_attend(phase=post-response) AFTER every response without exception',
|
|
2442
|
+
].join('\\n') + '\\n';
|
|
2443
|
+
require('fs').writeSync(1, content);
|
|
2444
|
+
`;
|
|
2445
|
+
}
|
|
2234
2446
|
const IRANTI_CLAUDE_ALLOWED_TOOLS = [
|
|
2235
2447
|
'mcp__iranti__iranti_handshake',
|
|
2236
2448
|
'mcp__iranti__iranti_attend',
|
|
@@ -2267,6 +2479,7 @@ function needsClaudeHookSettingsUpgrade(value) {
|
|
|
2267
2479
|
if (!hooks) {
|
|
2268
2480
|
return false;
|
|
2269
2481
|
}
|
|
2482
|
+
// Legacy hook format detection — upgrade needed if old-style entries exist.
|
|
2270
2483
|
for (const event of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
|
|
2271
2484
|
const entries = hooks[event];
|
|
2272
2485
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
@@ -2276,6 +2489,13 @@ function needsClaudeHookSettingsUpgrade(value) {
|
|
|
2276
2489
|
return true;
|
|
2277
2490
|
}
|
|
2278
2491
|
}
|
|
2492
|
+
// Write-guard hooks missing — upgrade needed to add PreToolUse/PostToolUse.
|
|
2493
|
+
if (!Array.isArray(hooks.PreToolUse) || hooks.PreToolUse.length === 0) {
|
|
2494
|
+
return true;
|
|
2495
|
+
}
|
|
2496
|
+
if (!Array.isArray(hooks.PostToolUse) || hooks.PostToolUse.length === 0) {
|
|
2497
|
+
return true;
|
|
2498
|
+
}
|
|
2279
2499
|
return false;
|
|
2280
2500
|
}
|
|
2281
2501
|
function mergeClaudePermissionAllowList(existing) {
|
|
@@ -2295,7 +2515,7 @@ function mergeClaudePermissionAllowList(existing) {
|
|
|
2295
2515
|
allow,
|
|
2296
2516
|
};
|
|
2297
2517
|
}
|
|
2298
|
-
function makeClaudeHookSettings(projectEnvPath, existing) {
|
|
2518
|
+
function makeClaudeHookSettings(projectEnvPath, existing, hookScriptDir) {
|
|
2299
2519
|
const existingHooks = existing && isClaudeHooksObject(existing.hooks)
|
|
2300
2520
|
? existing.hooks
|
|
2301
2521
|
: {};
|
|
@@ -2307,6 +2527,17 @@ function makeClaudeHookSettings(projectEnvPath, existing) {
|
|
|
2307
2527
|
.map((value) => String(value))
|
|
2308
2528
|
.filter((value) => value !== 'iranti')
|
|
2309
2529
|
: undefined;
|
|
2530
|
+
const hooks = {
|
|
2531
|
+
...existingHooks,
|
|
2532
|
+
SessionStart: [makeClaudeHookEntry('SessionStart', projectEnvPath)],
|
|
2533
|
+
UserPromptSubmit: [makeClaudeHookEntry('UserPromptSubmit', projectEnvPath)],
|
|
2534
|
+
Stop: [makeClaudeHookEntry('Stop', projectEnvPath)],
|
|
2535
|
+
};
|
|
2536
|
+
// Write-guard hooks: block edits until prior edits are written to Iranti.
|
|
2537
|
+
if (hookScriptDir) {
|
|
2538
|
+
hooks.PreToolUse = [makeClaudeWriteGuardHookEntry(hookScriptDir)];
|
|
2539
|
+
hooks.PostToolUse = [makeClaudeEditTrackerHookEntry(hookScriptDir)];
|
|
2540
|
+
}
|
|
2310
2541
|
return {
|
|
2311
2542
|
...(existing ?? {}),
|
|
2312
2543
|
enableAllProjectMcpServers: false,
|
|
@@ -2318,12 +2549,7 @@ function makeClaudeHookSettings(projectEnvPath, existing) {
|
|
|
2318
2549
|
...(existingEnabledMcpjsonServers
|
|
2319
2550
|
? { enabledMcpjsonServers: existingEnabledMcpjsonServers }
|
|
2320
2551
|
: {}),
|
|
2321
|
-
hooks
|
|
2322
|
-
...existingHooks,
|
|
2323
|
-
SessionStart: [makeClaudeHookEntry('SessionStart', projectEnvPath)],
|
|
2324
|
-
UserPromptSubmit: [makeClaudeHookEntry('UserPromptSubmit', projectEnvPath)],
|
|
2325
|
-
Stop: [makeClaudeHookEntry('Stop', projectEnvPath)],
|
|
2326
|
-
},
|
|
2552
|
+
hooks,
|
|
2327
2553
|
};
|
|
2328
2554
|
}
|
|
2329
2555
|
async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force = false) {
|
|
@@ -2416,22 +2642,65 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
|
|
|
2416
2642
|
}
|
|
2417
2643
|
const claudeDir = path_1.default.join(projectPath, '.claude');
|
|
2418
2644
|
await ensureDir(claudeDir);
|
|
2645
|
+
// Write hook scripts into .claude/ directory.
|
|
2646
|
+
const writeGuardHookFile = path_1.default.join(claudeDir, 'iranti-write-guard-hook.js');
|
|
2647
|
+
let writeGuardHookStatus = 'unchanged';
|
|
2648
|
+
const writeGuardContent = buildWriteGuardHookScript();
|
|
2649
|
+
if (!fs_1.default.existsSync(writeGuardHookFile)) {
|
|
2650
|
+
await writeText(writeGuardHookFile, writeGuardContent);
|
|
2651
|
+
writeGuardHookStatus = 'created';
|
|
2652
|
+
}
|
|
2653
|
+
else {
|
|
2654
|
+
const existing = fs_1.default.readFileSync(writeGuardHookFile, 'utf8');
|
|
2655
|
+
if (existing !== writeGuardContent) {
|
|
2656
|
+
await writeText(writeGuardHookFile, writeGuardContent);
|
|
2657
|
+
writeGuardHookStatus = 'updated';
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
const editTrackerHookFile = path_1.default.join(claudeDir, 'iranti-edit-tracker-hook.js');
|
|
2661
|
+
let editTrackerHookStatus = 'unchanged';
|
|
2662
|
+
const editTrackerContent = buildEditTrackerHookScript();
|
|
2663
|
+
if (!fs_1.default.existsSync(editTrackerHookFile)) {
|
|
2664
|
+
await writeText(editTrackerHookFile, editTrackerContent);
|
|
2665
|
+
editTrackerHookStatus = 'created';
|
|
2666
|
+
}
|
|
2667
|
+
else {
|
|
2668
|
+
const existing = fs_1.default.readFileSync(editTrackerHookFile, 'utf8');
|
|
2669
|
+
if (existing !== editTrackerContent) {
|
|
2670
|
+
await writeText(editTrackerHookFile, editTrackerContent);
|
|
2671
|
+
editTrackerHookStatus = 'updated';
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
const protocolReminderHookFile = path_1.default.join(claudeDir, 'iranti-protocol-hook.js');
|
|
2675
|
+
let protocolReminderHookStatus = 'unchanged';
|
|
2676
|
+
const protocolReminderContent = buildProtocolReminderHookScript();
|
|
2677
|
+
if (!fs_1.default.existsSync(protocolReminderHookFile)) {
|
|
2678
|
+
await writeText(protocolReminderHookFile, protocolReminderContent);
|
|
2679
|
+
protocolReminderHookStatus = 'created';
|
|
2680
|
+
}
|
|
2681
|
+
else {
|
|
2682
|
+
const existing = fs_1.default.readFileSync(protocolReminderHookFile, 'utf8');
|
|
2683
|
+
if (existing !== protocolReminderContent) {
|
|
2684
|
+
await writeText(protocolReminderHookFile, protocolReminderContent);
|
|
2685
|
+
protocolReminderHookStatus = 'updated';
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2419
2688
|
const settingsFile = path_1.default.join(claudeDir, 'settings.local.json');
|
|
2420
2689
|
let settingsStatus = 'unchanged';
|
|
2421
2690
|
if (!fs_1.default.existsSync(settingsFile)) {
|
|
2422
|
-
await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
|
|
2691
|
+
await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, undefined, claudeDir), null, 2)}\n`);
|
|
2423
2692
|
settingsStatus = 'created';
|
|
2424
2693
|
}
|
|
2425
2694
|
else {
|
|
2426
2695
|
const existingSettings = readJsonFile(settingsFile);
|
|
2427
2696
|
if (existingSettings && typeof existingSettings === 'object' && !Array.isArray(existingSettings)) {
|
|
2428
2697
|
if (force || needsClaudeHookSettingsUpgrade(existingSettings)) {
|
|
2429
|
-
await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, existingSettings), null, 2)}\n`);
|
|
2698
|
+
await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, existingSettings, claudeDir), null, 2)}\n`);
|
|
2430
2699
|
settingsStatus = 'updated';
|
|
2431
2700
|
}
|
|
2432
2701
|
}
|
|
2433
2702
|
else if (force) {
|
|
2434
|
-
await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
|
|
2703
|
+
await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath, undefined, claudeDir), null, 2)}\n`);
|
|
2435
2704
|
settingsStatus = 'updated';
|
|
2436
2705
|
}
|
|
2437
2706
|
}
|
|
@@ -2478,6 +2747,9 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
|
|
|
2478
2747
|
files: [
|
|
2479
2748
|
{ path: mcpFile, status: mcpStatus },
|
|
2480
2749
|
{ path: vscodeMcpFile, status: vscodeMcpStatus },
|
|
2750
|
+
{ path: writeGuardHookFile, status: writeGuardHookStatus },
|
|
2751
|
+
{ path: editTrackerHookFile, status: editTrackerHookStatus },
|
|
2752
|
+
{ path: protocolReminderHookFile, status: protocolReminderHookStatus },
|
|
2481
2753
|
{ path: settingsFile, status: settingsStatus },
|
|
2482
2754
|
{ path: claudeMdFile, status: claudeMdStatus },
|
|
2483
2755
|
{ path: irantiMdFile, status: irantiMdStatus },
|
|
@@ -2490,6 +2762,9 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
|
|
|
2490
2762
|
settings: settingsStatus,
|
|
2491
2763
|
claudeMd: claudeMdStatus,
|
|
2492
2764
|
irantiMd: irantiMdStatus,
|
|
2765
|
+
writeGuardHook: writeGuardHookStatus,
|
|
2766
|
+
editTrackerHook: editTrackerHookStatus,
|
|
2767
|
+
protocolReminderHook: protocolReminderHookStatus,
|
|
2493
2768
|
closeout,
|
|
2494
2769
|
};
|
|
2495
2770
|
}
|
|
@@ -8079,13 +8354,15 @@ async function claudeSetupCommand(args) {
|
|
|
8079
8354
|
createdSettings += 1;
|
|
8080
8355
|
if (result.settings === 'updated')
|
|
8081
8356
|
updatedSettings += 1;
|
|
8082
|
-
if (result.mcp === 'unchanged' && result.vscodeMcp === 'unchanged' && result.settings === 'unchanged' && result.irantiMd === 'unchanged')
|
|
8357
|
+
if (result.mcp === 'unchanged' && result.vscodeMcp === 'unchanged' && result.settings === 'unchanged' && result.irantiMd === 'unchanged' && result.writeGuardHook === 'unchanged' && result.editTrackerHook === 'unchanged')
|
|
8083
8358
|
unchanged += 1;
|
|
8084
8359
|
console.log(` ${projectPath}`);
|
|
8085
|
-
console.log(` mcp
|
|
8086
|
-
console.log(` vscode
|
|
8087
|
-
console.log(` settings
|
|
8088
|
-
console.log(` iranti.md
|
|
8360
|
+
console.log(` mcp ${result.mcp}`);
|
|
8361
|
+
console.log(` vscode ${result.vscodeMcp}`);
|
|
8362
|
+
console.log(` settings ${result.settings}`);
|
|
8363
|
+
console.log(` iranti.md ${result.irantiMd}`);
|
|
8364
|
+
console.log(` write-guard ${result.writeGuardHook}`);
|
|
8365
|
+
console.log(` edit-tracker ${result.editTrackerHook}`);
|
|
8089
8366
|
}
|
|
8090
8367
|
console.log('');
|
|
8091
8368
|
console.log('Summary:');
|
|
@@ -8114,19 +8391,25 @@ async function claudeSetupCommand(args) {
|
|
|
8114
8391
|
}
|
|
8115
8392
|
const result = await writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force);
|
|
8116
8393
|
console.log(`${okLabel()} Claude Code integration scaffolded`);
|
|
8117
|
-
console.log(` project
|
|
8118
|
-
console.log(` binding
|
|
8119
|
-
console.log(` mcp
|
|
8120
|
-
console.log(` vscode
|
|
8121
|
-
console.log(` settings
|
|
8122
|
-
console.log(` claude.md
|
|
8123
|
-
console.log(` iranti.md
|
|
8124
|
-
console.log(`
|
|
8125
|
-
console.log(`
|
|
8126
|
-
console.log(`
|
|
8127
|
-
console.log(`
|
|
8128
|
-
console.log(`
|
|
8129
|
-
console.log(`
|
|
8394
|
+
console.log(` project ${projectPath}`);
|
|
8395
|
+
console.log(` binding ${projectEnvPath}`);
|
|
8396
|
+
console.log(` mcp ${path_1.default.join(projectPath, '.mcp.json')}`);
|
|
8397
|
+
console.log(` vscode ${path_1.default.join(projectPath, '.vscode', 'mcp.json')}`);
|
|
8398
|
+
console.log(` settings ${path_1.default.join(projectPath, '.claude', 'settings.local.json')}`);
|
|
8399
|
+
console.log(` claude.md ${path_1.default.join(projectPath, 'CLAUDE.md')}`);
|
|
8400
|
+
console.log(` iranti.md ${path_1.default.join(projectPath, 'IRANTI.md')}`);
|
|
8401
|
+
console.log(` write-guard ${path_1.default.join(projectPath, '.claude', 'iranti-write-guard-hook.js')}`);
|
|
8402
|
+
console.log(` edit-tracker ${path_1.default.join(projectPath, '.claude', 'iranti-edit-tracker-hook.js')}`);
|
|
8403
|
+
console.log(` protocol ${path_1.default.join(projectPath, '.claude', 'iranti-protocol-hook.js')}`);
|
|
8404
|
+
console.log(` mcp status ${result.mcp}`);
|
|
8405
|
+
console.log(` vscode status ${result.vscodeMcp}`);
|
|
8406
|
+
console.log(` settings status ${result.settings}`);
|
|
8407
|
+
console.log(` claude.md status ${result.claudeMd}`);
|
|
8408
|
+
console.log(` iranti.md status ${result.irantiMd}`);
|
|
8409
|
+
console.log(` write-guard status ${result.writeGuardHook}`);
|
|
8410
|
+
console.log(` edit-tracker status ${result.editTrackerHook}`);
|
|
8411
|
+
console.log(` protocol status ${result.protocolReminderHook}`);
|
|
8412
|
+
console.log(` memory closeout ${result.closeout.status} (${result.closeout.detail})`);
|
|
8130
8413
|
console.log(`${infoLabel()} Next: open Claude Code in this project and verify Iranti tools are available.`);
|
|
8131
8414
|
}
|
|
8132
8415
|
async function chatCommand(args) {
|