harnery 0.32.0 → 0.33.0

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.
Files changed (91) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +3 -1
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +212 -1
  6. package/dist/commands/claude-desktop.d.ts +2 -2
  7. package/dist/commands/claude-desktop.d.ts.map +1 -1
  8. package/dist/commands/claude-desktop.js +74 -3
  9. package/dist/commands/doctor.d.ts +4 -2
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +45 -6
  12. package/dist/commands/init.d.ts +1 -0
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +10 -0
  15. package/dist/commands/instructions.d.ts +4 -0
  16. package/dist/commands/instructions.d.ts.map +1 -0
  17. package/dist/commands/instructions.js +100 -0
  18. package/dist/commands/presence.d.ts.map +1 -1
  19. package/dist/commands/presence.js +4 -1
  20. package/dist/core/agents/canonical-emit.d.ts +1 -1
  21. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  22. package/dist/core/agents/canonical-emit.js +5 -3
  23. package/dist/core/agents/cli.js +5 -1
  24. package/dist/core/agents/coord-client.d.ts +11 -0
  25. package/dist/core/agents/coord-client.d.ts.map +1 -1
  26. package/dist/core/agents/index.d.ts +1 -0
  27. package/dist/core/agents/index.d.ts.map +1 -1
  28. package/dist/core/agents/index.js +1 -0
  29. package/dist/core/agents/render/prompt-context.d.ts +6 -0
  30. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/prompt-context.js +46 -5
  32. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  33. package/dist/core/agents/render/session-context.js +8 -2
  34. package/dist/core/agents/rules/stop-hook.js +4 -4
  35. package/dist/core/agents/state/heartbeat-projector.d.ts +11 -0
  36. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  37. package/dist/core/agents/state/heartbeat-projector.js +54 -0
  38. package/dist/core/agents/state/heartbeat-writer.d.ts +15 -0
  39. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  40. package/dist/core/agents/state/heartbeat-writer.js +32 -0
  41. package/dist/core/agents/state/session-state.d.ts +44 -0
  42. package/dist/core/agents/state/session-state.d.ts.map +1 -0
  43. package/dist/core/agents/state/session-state.js +81 -0
  44. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  45. package/dist/core/hooks/adapter/events.js +1 -0
  46. package/dist/core/hooks/adapter/parse.d.ts +1 -1
  47. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  48. package/dist/core/hooks/adapter/parse.js +2 -0
  49. package/dist/core/hooks/cli.js +90 -6
  50. package/dist/core/hooks/codex-authorization.d.ts +30 -0
  51. package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
  52. package/dist/core/hooks/codex-authorization.js +199 -0
  53. package/dist/core/hooks/events/schema.d.ts +30 -1
  54. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  55. package/dist/core/presence/blob.d.ts +4 -0
  56. package/dist/core/presence/blob.d.ts.map +1 -1
  57. package/dist/core/presence/blob.js +12 -0
  58. package/dist/core/presence/index.js +15 -1
  59. package/dist/lib/claude-desktop.d.ts +47 -0
  60. package/dist/lib/claude-desktop.d.ts.map +1 -1
  61. package/dist/lib/claude-desktop.js +169 -1
  62. package/dist/lib/instructions/bundle.d.ts +33 -0
  63. package/dist/lib/instructions/bundle.d.ts.map +1 -0
  64. package/dist/lib/instructions/bundle.js +149 -0
  65. package/package.json +2 -2
  66. package/src/commander.ts +3 -1
  67. package/src/commands/agents.ts +248 -1
  68. package/src/commands/claude-desktop.ts +106 -5
  69. package/src/commands/doctor.ts +59 -7
  70. package/src/commands/init.ts +11 -0
  71. package/src/commands/instructions.ts +129 -0
  72. package/src/commands/presence.ts +7 -1
  73. package/src/core/agents/canonical-emit.ts +6 -4
  74. package/src/core/agents/cli.ts +6 -1
  75. package/src/core/agents/coord-client.ts +11 -0
  76. package/src/core/agents/index.ts +1 -0
  77. package/src/core/agents/render/prompt-context.ts +71 -6
  78. package/src/core/agents/render/session-context.ts +14 -2
  79. package/src/core/agents/rules/stop-hook.ts +5 -5
  80. package/src/core/agents/state/heartbeat-projector.ts +63 -0
  81. package/src/core/agents/state/heartbeat-writer.ts +51 -0
  82. package/src/core/agents/state/session-state.ts +131 -0
  83. package/src/core/hooks/adapter/events.ts +1 -0
  84. package/src/core/hooks/adapter/parse.ts +3 -0
  85. package/src/core/hooks/cli.ts +98 -7
  86. package/src/core/hooks/codex-authorization.ts +253 -0
  87. package/src/core/hooks/events/schema.ts +44 -0
  88. package/src/core/presence/blob.ts +18 -0
  89. package/src/core/presence/index.ts +18 -1
  90. package/src/lib/claude-desktop.ts +232 -1
  91. package/src/lib/instructions/bundle.ts +199 -0
@@ -0,0 +1,199 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import type { Adapter } from "../../core/hooks/events/schema.ts";
5
+
6
+ export type InstructionComponentRole = "always_loaded" | "just_in_time" | "runtime_config";
7
+
8
+ export interface InstructionBundleComponent {
9
+ path: string;
10
+ role: InstructionComponentRole;
11
+ bytes: number;
12
+ sha256: string;
13
+ }
14
+
15
+ export interface InstructionBundle {
16
+ schema_version: 1;
17
+ adapter: Adapter;
18
+ coord_root: string;
19
+ profile_root: string;
20
+ instruction_roots: string[];
21
+ instruction_bundle_id: string;
22
+ canonical_source_id: string;
23
+ components: InstructionBundleComponent[];
24
+ canonical_sources: InstructionBundleComponent[];
25
+ }
26
+
27
+ interface BuildBundleOptions {
28
+ coordRoot: string;
29
+ cwd: string;
30
+ adapter: Adapter;
31
+ }
32
+
33
+ interface Candidate {
34
+ absolutePath: string;
35
+ role: InstructionComponentRole;
36
+ }
37
+
38
+ /**
39
+ * Build the content identity for the repo-authored instruction surface an
40
+ * adapter can load at one working path. The identity intentionally excludes
41
+ * timestamps and git revisions: two checkouts with byte-identical effective
42
+ * instructions belong to the same cohort.
43
+ */
44
+ export function buildInstructionBundle(options: BuildBundleOptions): InstructionBundle {
45
+ const coordRoot = resolve(options.coordRoot);
46
+ const cwd = resolve(options.cwd);
47
+ const roots = resolveInstructionRoots(coordRoot, cwd);
48
+ const profileRoot = roots.at(-1) ?? coordRoot;
49
+ const components = dedupeCandidates(
50
+ roots.flatMap((root) => adapterCandidates(root, options.adapter)),
51
+ coordRoot,
52
+ );
53
+ const canonicalSources = dedupeCandidates(
54
+ roots.flatMap((root) => canonicalCandidates(root)),
55
+ coordRoot,
56
+ );
57
+
58
+ return {
59
+ schema_version: 1,
60
+ adapter: options.adapter,
61
+ coord_root: coordRoot,
62
+ profile_root: relativePath(coordRoot, profileRoot),
63
+ instruction_roots: roots.map((root) => relativePath(coordRoot, root)),
64
+ instruction_bundle_id: identity("instruction-bundle-v1", options.adapter, components),
65
+ canonical_source_id: identity("instruction-source-v1", null, canonicalSources),
66
+ components,
67
+ canonical_sources: canonicalSources,
68
+ };
69
+ }
70
+
71
+ function resolveInstructionRoots(coordRoot: string, cwd: string): string[] {
72
+ const roots = [coordRoot];
73
+ if (!isWithin(coordRoot, cwd)) return roots;
74
+
75
+ const ancestors: string[] = [];
76
+ let cursor = cwd;
77
+ while (cursor !== coordRoot) {
78
+ if (existsSync(join(cursor, "AGENTS.md")) || existsSync(join(cursor, "CLAUDE.md"))) {
79
+ ancestors.push(cursor);
80
+ }
81
+ const parent = dirname(cursor);
82
+ if (parent === cursor) break;
83
+ cursor = parent;
84
+ }
85
+ roots.push(...ancestors.reverse().filter((root) => root !== coordRoot));
86
+ return roots;
87
+ }
88
+
89
+ function adapterCandidates(root: string, adapter: Adapter): Candidate[] {
90
+ const candidates: Candidate[] = [];
91
+ const addFile = (path: string, role: InstructionComponentRole) => {
92
+ if (existsSync(path) && lstatSync(path).isFile()) candidates.push({ absolutePath: path, role });
93
+ };
94
+ const addTree = (path: string, role: InstructionComponentRole) => {
95
+ for (const file of walkFiles(path)) candidates.push({ absolutePath: file, role });
96
+ };
97
+
98
+ if (adapter === "claude-code") {
99
+ addFile(join(root, "CLAUDE.md"), "always_loaded");
100
+ addTree(join(root, ".claude", "rules"), "always_loaded");
101
+ addTree(join(root, ".claude", "skills"), "just_in_time");
102
+ addFile(join(root, ".claude", "settings.json"), "runtime_config");
103
+ addFile(join(root, ".claude", "settings.local.json"), "runtime_config");
104
+ } else if (adapter === "cursor") {
105
+ addFile(join(root, "AGENTS.md"), "always_loaded");
106
+ addTree(join(root, ".cursor", "rules"), "always_loaded");
107
+ addTree(join(root, ".agents", "skills"), "just_in_time");
108
+ addTree(join(root, ".cursor", "skills"), "just_in_time");
109
+ addFile(join(root, ".cursor", "hooks.json"), "runtime_config");
110
+ } else {
111
+ addFile(join(root, "AGENTS.md"), "always_loaded");
112
+ addTree(join(root, ".harnery", "skills"), "just_in_time");
113
+ addFile(join(root, ".codex", "hooks.json"), "runtime_config");
114
+ }
115
+
116
+ // Harnery's host configuration governs hook enforcement for all adapters.
117
+ addFile(join(root, ".harnery", "config.jsonc"), "runtime_config");
118
+ return candidates;
119
+ }
120
+
121
+ function canonicalCandidates(root: string): Candidate[] {
122
+ const candidates: Candidate[] = [];
123
+ const agents = join(root, "AGENTS.md");
124
+ if (existsSync(agents) && lstatSync(agents).isFile()) {
125
+ candidates.push({ absolutePath: agents, role: "always_loaded" });
126
+ }
127
+ for (const file of walkFiles(join(root, ".agents"))) {
128
+ candidates.push({ absolutePath: file, role: "just_in_time" });
129
+ }
130
+ return candidates;
131
+ }
132
+
133
+ function walkFiles(root: string): string[] {
134
+ if (!existsSync(root) || !lstatSync(root).isDirectory()) return [];
135
+ const files: string[] = [];
136
+ const visit = (dir: string) => {
137
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
138
+ a.name.localeCompare(b.name),
139
+ )) {
140
+ const path = join(dir, entry.name);
141
+ if (entry.isDirectory()) visit(path);
142
+ else if (entry.isFile()) files.push(path);
143
+ }
144
+ };
145
+ visit(root);
146
+ return files;
147
+ }
148
+
149
+ function dedupeCandidates(
150
+ candidates: Candidate[],
151
+ coordRoot: string,
152
+ ): InstructionBundleComponent[] {
153
+ const byPath = new Map<string, Candidate>();
154
+ for (const candidate of candidates) {
155
+ const key = resolve(candidate.absolutePath);
156
+ const prior = byPath.get(key);
157
+ if (!prior || roleRank(candidate.role) < roleRank(prior.role)) byPath.set(key, candidate);
158
+ }
159
+
160
+ return [...byPath.values()]
161
+ .map(({ absolutePath, role }) => {
162
+ const bytes = readFileSync(absolutePath);
163
+ return {
164
+ path: relativePath(coordRoot, absolutePath),
165
+ role,
166
+ bytes: bytes.byteLength,
167
+ sha256: createHash("sha256").update(bytes).digest("hex"),
168
+ };
169
+ })
170
+ .sort((a, b) => a.path.localeCompare(b.path) || a.role.localeCompare(b.role));
171
+ }
172
+
173
+ function identity(
174
+ prefix: string,
175
+ adapter: Adapter | null,
176
+ components: InstructionBundleComponent[],
177
+ ): string {
178
+ const body = components.map(({ path, role, sha256 }) => ({ path, role, sha256 }));
179
+ return `sha256:${createHash("sha256")
180
+ .update(JSON.stringify({ prefix, ...(adapter ? { adapter } : {}), components: body }))
181
+ .digest("hex")}`;
182
+ }
183
+
184
+ function relativePath(root: string, path: string): string {
185
+ const rel = relative(root, path);
186
+ return rel === "" ? "." : rel.split(sep).join("/");
187
+ }
188
+
189
+ function isWithin(root: string, path: string): boolean {
190
+ if (!isAbsolute(root) || !isAbsolute(path)) return false;
191
+ const rel = relative(root, path);
192
+ return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== "..");
193
+ }
194
+
195
+ function roleRank(role: InstructionComponentRole): number {
196
+ if (role === "always_loaded") return 0;
197
+ if (role === "runtime_config") return 1;
198
+ return 2;
199
+ }