intentdna 1.8.4 → 1.8.5
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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/dist/cli/commands/sync.js +10 -8
- package/dist/runtime/claude-sync-target.d.ts +3 -1
- package/dist/runtime/claude-sync-target.js +16 -4
- package/dist/runtime/settings-adapter.d.ts +12 -0
- package/dist/runtime/settings-adapter.js +119 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "intentdna",
|
|
11
11
|
"description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
|
|
12
|
-
"version": "1.8.
|
|
12
|
+
"version": "1.8.5",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Samuel"
|
|
15
15
|
},
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"version": "1.8.
|
|
28
|
+
"version": "1.8.5"
|
|
29
29
|
}
|
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ IntentDNA is not a model company, generic agent framework, prompt library, canva
|
|
|
17
17
|
## Install or update
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install --global intentdna@1.8.
|
|
20
|
+
npm install --global intentdna@1.8.5
|
|
21
21
|
dna --version
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Version `1.8.
|
|
24
|
+
Version `1.8.5` keeps Claude plugin Hook delivery in `hooks/hooks.json`: plugin sync does not register project `settings.json` Hooks, retracts exact stale bin-mode `dna-hook` entries once, and leaves user settings untouched in steady state. Online generates copyable local commands only; you still run and review them in your own project.
|
|
25
25
|
|
|
26
26
|
## Product Shape
|
|
27
27
|
|
|
@@ -612,6 +612,13 @@ export async function runSync(opts) {
|
|
|
612
612
|
opts.skillsDir = resolve(projectDir, ".agents", "skills");
|
|
613
613
|
process.stderr.write("Codex workflow projection: .agents/skills\n");
|
|
614
614
|
}
|
|
615
|
+
const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
|
|
616
|
+
let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
|
|
617
|
+
if (!opts.remove && requestedMode === "auto") {
|
|
618
|
+
resolvedMode = await detectMode();
|
|
619
|
+
process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
|
|
620
|
+
}
|
|
621
|
+
opts.plugin = resolvedMode === "plugin";
|
|
615
622
|
if (!opts.remove && !opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.settingsPath && !opts.codex) {
|
|
616
623
|
// Auto-detect Claude Code environment → fill all outputs
|
|
617
624
|
const hasClaudeDir = await fileExists(resolve(projectDir, ".claude"));
|
|
@@ -621,16 +628,11 @@ export async function runSync(opts) {
|
|
|
621
628
|
opts.agentsDir = resolve(projectDir, ".claude", "agents");
|
|
622
629
|
opts.skillsDir = resolve(projectDir, ".claude", "skills");
|
|
623
630
|
opts.settingsPath = resolve(projectDir, ".claude", "settings.json");
|
|
624
|
-
process.stderr.write(
|
|
631
|
+
process.stderr.write(resolvedMode === "plugin"
|
|
632
|
+
? "Auto-detect: Claude Code → CLAUDE.md + agents + skills; plugin hooks (no settings injection)\n"
|
|
633
|
+
: "Auto-detect: Claude Code → CLAUDE.md + agents + skills + settings\n");
|
|
625
634
|
}
|
|
626
635
|
}
|
|
627
|
-
const requestedMode = opts.mode ?? (opts.plugin === true ? "plugin" : opts.plugin === false ? "bin" : "auto");
|
|
628
|
-
let resolvedMode = requestedMode === "auto" ? "bin" : requestedMode;
|
|
629
|
-
if (!opts.remove && requestedMode === "auto") {
|
|
630
|
-
resolvedMode = await detectMode();
|
|
631
|
-
process.stderr.write(`Auto-detect: ${resolvedMode} mode${resolvedMode === "plugin" ? " (intentdna registered as Claude Code plugin)" : ""}\n`);
|
|
632
|
-
}
|
|
633
|
-
opts.plugin = resolvedMode === "plugin";
|
|
634
636
|
// Handle --remove
|
|
635
637
|
if (opts.remove) {
|
|
636
638
|
if (!opts.inject && !opts.hooksDir && !opts.agentsDir && !opts.workflowDir && !opts.skillsDir && !opts.codex) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConstraintIR, RoleDef } from "../schema/types.js";
|
|
2
2
|
import type { SyncOutputArtifactId } from "./output-artifact-registry.js";
|
|
3
3
|
import { type CompiledIRFile } from "./plugin-adapter.js";
|
|
4
|
-
import { type DNASettings, type HookEventKey } from "./settings-adapter.js";
|
|
4
|
+
import { type DNASettings, type HookEventKey, type IntentDNASettingsHookCleanupResult } from "./settings-adapter.js";
|
|
5
5
|
import type { AdapterCapabilityReport, CapabilityReportEntry, PlannedArtifact, SyncTargetPlan } from "./sync-target-plan.js";
|
|
6
6
|
export type ClaudeSyncMode = "plugin" | "bin";
|
|
7
7
|
export interface ClaudeCorePlannedArtifact extends PlannedArtifact {
|
|
@@ -31,6 +31,7 @@ export interface ClaudeCoreSyncTargetPlan extends SyncTargetPlan {
|
|
|
31
31
|
legacyHooksDir: string;
|
|
32
32
|
mcpConfigPath: string;
|
|
33
33
|
settingsHookRegistration?: ClaudeSettingsHookRegistration;
|
|
34
|
+
pluginSettingsPath?: string;
|
|
34
35
|
pluginSkipsSettings: boolean;
|
|
35
36
|
plannedArtifacts: ClaudeCorePlannedArtifact[];
|
|
36
37
|
capabilityReport: ClaudeCoreAdapterCapabilityReport;
|
|
@@ -38,6 +39,7 @@ export interface ClaudeCoreSyncTargetPlan extends SyncTargetPlan {
|
|
|
38
39
|
export interface ExecuteClaudeCoreSyncTargetPlanResult {
|
|
39
40
|
messages: string[];
|
|
40
41
|
removedLegacyHooks: string[];
|
|
42
|
+
settingsHookCleanup?: IntentDNASettingsHookCleanupResult;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Remove legacy bash hook scripts written by previous Intent DNA versions.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { createCompiledIR, writeCompiledIR } from "./plugin-adapter.js";
|
|
4
|
-
import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "./settings-adapter.js";
|
|
4
|
+
import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts, removeIntentDNASettingsHooks, } from "./settings-adapter.js";
|
|
5
5
|
import { formatSyncTargetPlanDiagnostics } from "./sync-target-plan.js";
|
|
6
6
|
const LEGACY_BASH_HOOKS = [
|
|
7
7
|
"dna-pre-tool-use.sh",
|
|
@@ -83,6 +83,7 @@ export function createClaudeCoreSyncTargetPlan(options) {
|
|
|
83
83
|
legacyHooksDir,
|
|
84
84
|
mcpConfigPath,
|
|
85
85
|
settingsHookRegistration,
|
|
86
|
+
pluginSettingsPath: options.mode === "plugin" ? options.settingsPath : undefined,
|
|
86
87
|
pluginSkipsSettings: options.mode === "plugin" && !!options.settingsPath,
|
|
87
88
|
plannedArtifacts,
|
|
88
89
|
capabilityReport: createClaudeCapabilityReport(plannedArtifacts, options.mode, settingsHookRegistration),
|
|
@@ -238,6 +239,7 @@ async function registerDNAMCPServer(projectDir) {
|
|
|
238
239
|
}
|
|
239
240
|
export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
|
|
240
241
|
const messages = [];
|
|
242
|
+
let settingsHookCleanup;
|
|
241
243
|
const writtenIrPath = await writeCompiledIR(projectDir, plan.compiledIR);
|
|
242
244
|
messages.push(`Wrote compiled IR: ${writtenIrPath}`);
|
|
243
245
|
const removedLegacyHooks = await removeLegacyBashHooks(plan.legacyHooksDir);
|
|
@@ -253,9 +255,19 @@ export async function executeClaudeCoreSyncTargetPlan(plan, projectDir) {
|
|
|
253
255
|
messages.push(`Bin mode: registered ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
|
|
254
256
|
}
|
|
255
257
|
else if (plan.pluginSkipsSettings) {
|
|
256
|
-
|
|
258
|
+
settingsHookCleanup = await removeIntentDNASettingsHooks(plan.pluginSettingsPath);
|
|
259
|
+
if (settingsHookCleanup.removedCommands > 0) {
|
|
260
|
+
const action = settingsHookCleanup.removedFile ? "removed" : "cleaned";
|
|
261
|
+
messages.push(`Plugin mode: ${action} ${settingsHookCleanup.removedCommands} stale bin-mode dna-hook command(s) from ${plan.pluginSettingsPath}; hooks are managed by plugin framework`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
messages.push("Plugin mode: hooks managed by plugin framework (settings.json not written)");
|
|
265
|
+
}
|
|
266
|
+
if (settingsHookCleanup.warning) {
|
|
267
|
+
messages.push(`Plugin mode: preserved settings.json without cleanup (${settingsHookCleanup.warning})`);
|
|
268
|
+
}
|
|
257
269
|
}
|
|
258
|
-
return { messages, removedLegacyHooks };
|
|
270
|
+
return { messages, removedLegacyHooks, settingsHookCleanup };
|
|
259
271
|
}
|
|
260
272
|
export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
|
|
261
273
|
const lines = [
|
|
@@ -267,7 +279,7 @@ export function formatClaudeCoreSyncTargetPlanDryRun(plan) {
|
|
|
267
279
|
lines.push(`Dry-run: would register ${plan.settingsHookRegistration.enabledEvents.length} hook event(s) in ${plan.settingsHookRegistration.path}`);
|
|
268
280
|
}
|
|
269
281
|
else if (plan.pluginSkipsSettings) {
|
|
270
|
-
lines.push("Plugin mode: hooks managed by plugin framework
|
|
282
|
+
lines.push("Plugin mode: hooks managed by plugin framework; would remove stale bin-mode dna-hook commands if present and would not register settings hooks");
|
|
271
283
|
}
|
|
272
284
|
const capabilityEntries = Object.entries(plan.capabilityReport.capabilities)
|
|
273
285
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
@@ -18,6 +18,13 @@ export interface SettingsHookEntry {
|
|
|
18
18
|
export interface DNASettings {
|
|
19
19
|
hooks: Record<string, SettingsHookEntry[]>;
|
|
20
20
|
}
|
|
21
|
+
export interface IntentDNASettingsHookCleanupResult {
|
|
22
|
+
changed: boolean;
|
|
23
|
+
removedCommands: number;
|
|
24
|
+
removedEvents: string[];
|
|
25
|
+
removedFile: boolean;
|
|
26
|
+
warning?: string;
|
|
27
|
+
}
|
|
21
28
|
/**
|
|
22
29
|
* Compile settings.json hook configuration for dna-hook binary.
|
|
23
30
|
* Uses `dna-hook <event>` commands.
|
|
@@ -34,3 +41,8 @@ export declare function detectEnabledEvents(ir: import("../schema/types.js").Con
|
|
|
34
41
|
* Replaces DNA hooks (identified by "dna-" prefix), keeps user hooks.
|
|
35
42
|
*/
|
|
36
43
|
export declare function mergeSettingsFile(dnaSettings: DNASettings, settingsPath: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Remove bin-mode IntentDNA hook commands when Claude plugin hooks own dispatch.
|
|
46
|
+
* Invalid or user-owned settings are preserved without rewriting the file.
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeIntentDNASettingsHooks(settingsPath: string): Promise<IntentDNASettingsHookCleanupResult>;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* See docs/archive/2026H1/references/cc-source-analysis.md §2 for the full hook event catalog.
|
|
8
8
|
*/
|
|
9
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
10
10
|
import { getHookEventBySettingsKey, settingsHookEventKeys } from "../hooks/event-registry.js";
|
|
11
|
-
// ──
|
|
11
|
+
// ── Bin Mode (project settings via dna-hook binary) ───────
|
|
12
12
|
/**
|
|
13
13
|
* Compile settings.json hook configuration for dna-hook binary.
|
|
14
14
|
* Uses `dna-hook <event>` commands.
|
|
@@ -131,3 +131,120 @@ export async function mergeSettingsFile(dnaSettings, settingsPath) {
|
|
|
131
131
|
existing.hooks = mergedHooks;
|
|
132
132
|
await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
133
133
|
}
|
|
134
|
+
const INTENTDNA_SETTINGS_HOOK_COMMANDS = new Set(settingsHookEventKeys().map((key) => `dna-hook ${getHookEventBySettingsKey(key).event}`));
|
|
135
|
+
function isIntentDNASettingsHookCommand(value, eventName) {
|
|
136
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
137
|
+
return false;
|
|
138
|
+
const hook = value;
|
|
139
|
+
const expectedCommand = `dna-hook ${eventName}`;
|
|
140
|
+
return hook.type === "command"
|
|
141
|
+
&& typeof hook.command === "string"
|
|
142
|
+
&& hook.command === expectedCommand
|
|
143
|
+
&& INTENTDNA_SETTINGS_HOOK_COMMANDS.has(expectedCommand);
|
|
144
|
+
}
|
|
145
|
+
function isGeneratedIntentDNASettingsHookEntry(entry) {
|
|
146
|
+
return entry.matcher === ""
|
|
147
|
+
&& Object.keys(entry).every((key) => key === "matcher" || key === "hooks");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Remove bin-mode IntentDNA hook commands when Claude plugin hooks own dispatch.
|
|
151
|
+
* Invalid or user-owned settings are preserved without rewriting the file.
|
|
152
|
+
*/
|
|
153
|
+
export async function removeIntentDNASettingsHooks(settingsPath) {
|
|
154
|
+
const unchanged = (warning) => ({
|
|
155
|
+
changed: false,
|
|
156
|
+
removedCommands: 0,
|
|
157
|
+
removedEvents: [],
|
|
158
|
+
removedFile: false,
|
|
159
|
+
...(warning ? { warning } : {}),
|
|
160
|
+
});
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await readFile(settingsPath, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
167
|
+
return code === "ENOENT" ? unchanged() : unchanged("could not read settings.json");
|
|
168
|
+
}
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(raw);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return unchanged("settings.json is not valid JSON");
|
|
175
|
+
}
|
|
176
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
177
|
+
return unchanged("settings.json root is not an object");
|
|
178
|
+
}
|
|
179
|
+
const settings = parsed;
|
|
180
|
+
const rawHooks = settings.hooks;
|
|
181
|
+
if (!rawHooks || typeof rawHooks !== "object" || Array.isArray(rawHooks)) {
|
|
182
|
+
return unchanged();
|
|
183
|
+
}
|
|
184
|
+
const cleanedHooks = {};
|
|
185
|
+
const removedEvents = [];
|
|
186
|
+
let removedCommands = 0;
|
|
187
|
+
for (const [eventName, rawEntries] of Object.entries(rawHooks)) {
|
|
188
|
+
if (!Array.isArray(rawEntries)) {
|
|
189
|
+
cleanedHooks[eventName] = rawEntries;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let eventRemoved = false;
|
|
193
|
+
const cleanedEntries = [];
|
|
194
|
+
for (const rawEntry of rawEntries) {
|
|
195
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
196
|
+
cleanedEntries.push(rawEntry);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const entry = rawEntry;
|
|
200
|
+
if (!Array.isArray(entry.hooks)) {
|
|
201
|
+
cleanedEntries.push(rawEntry);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let entryRemovedCommands = 0;
|
|
205
|
+
const retainedCommands = entry.hooks.filter((hook) => {
|
|
206
|
+
const owned = isIntentDNASettingsHookCommand(hook, eventName);
|
|
207
|
+
if (owned) {
|
|
208
|
+
removedCommands += 1;
|
|
209
|
+
entryRemovedCommands += 1;
|
|
210
|
+
eventRemoved = true;
|
|
211
|
+
}
|
|
212
|
+
return !owned;
|
|
213
|
+
});
|
|
214
|
+
if (entryRemovedCommands === 0) {
|
|
215
|
+
cleanedEntries.push(rawEntry);
|
|
216
|
+
}
|
|
217
|
+
else if (retainedCommands.length > 0 || !isGeneratedIntentDNASettingsHookEntry(entry)) {
|
|
218
|
+
cleanedEntries.push({ ...entry, hooks: retainedCommands });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (eventRemoved)
|
|
222
|
+
removedEvents.push(eventName);
|
|
223
|
+
if (!eventRemoved || cleanedEntries.length > 0)
|
|
224
|
+
cleanedHooks[eventName] = cleanedEntries;
|
|
225
|
+
}
|
|
226
|
+
if (removedCommands === 0)
|
|
227
|
+
return unchanged();
|
|
228
|
+
if (Object.keys(cleanedHooks).length > 0) {
|
|
229
|
+
settings.hooks = cleanedHooks;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
delete settings.hooks;
|
|
233
|
+
}
|
|
234
|
+
if (Object.keys(settings).length === 0) {
|
|
235
|
+
await unlink(settingsPath);
|
|
236
|
+
return {
|
|
237
|
+
changed: true,
|
|
238
|
+
removedCommands,
|
|
239
|
+
removedEvents,
|
|
240
|
+
removedFile: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
244
|
+
return {
|
|
245
|
+
changed: true,
|
|
246
|
+
removedCommands,
|
|
247
|
+
removedEvents,
|
|
248
|
+
removedFile: false,
|
|
249
|
+
};
|
|
250
|
+
}
|