@kylecheng3146/agent-ops 0.1.2 → 0.1.4
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 +11 -2
- package/dist/packages/cli/src/bin.js +184 -269
- package/dist/packages/cli/src/cli.js +2 -7
- package/dist/packages/cli/src/commands/hook.js +42 -0
- package/dist/packages/cli/src/commands/init.js +24 -3
- package/dist/packages/cli/src/commands/update.js +4 -1
- package/dist/packages/cli/src/context.js +102 -0
- package/dist/packages/cli/src/hook-entry.js +10 -0
- package/dist/packages/cli/src/hook-process.js +54 -0
- package/dist/packages/cli/src/ui.js +373 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/wizard.js +50 -0
- package/dist/runtime/src/adapters/claude/config.js +19 -0
- package/dist/runtime/src/adapters/codex/config.js +16 -0
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/hooks.js +77 -0
- package/dist/runtime/src/install/ownership.js +20 -0
- package/dist/runtime/src/install/plan.js +27 -2
- package/dist/runtime/src/install/uninstall.js +28 -0
- package/dist/runtime/src/install/update.js +3 -0
- package/dist/runtime/src/schema/validate.js +57 -0
- package/dist/runtime/src/security/permissions.js +22 -3
- package/package.json +5 -3
- package/postinstall.cjs +102 -0
- package/schemas/manifest.schema.json +33 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { buildClaudeHookSettings, mergeClaudeSettings, stripClaudeManagedHooks } from "../adapters/claude/config.js";
|
|
2
|
+
import { buildCodexHookConfig, mergeCodexHookConfig, stripCodexManagedHooks } from "../adapters/codex/config.js";
|
|
3
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
4
|
+
export const CLAUDE_HOOK_PATH = ".claude/settings.json";
|
|
5
|
+
export const CODEX_HOOK_PATH = ".codex/hooks.json";
|
|
6
|
+
function format(value) {
|
|
7
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
8
|
+
}
|
|
9
|
+
function parseSettings(path, source) {
|
|
10
|
+
if (source === null || source.trim().length === 0) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(source);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new AgentOpsError("HOOK_SETTINGS_INVALID_JSON", `Hook settings are not valid JSON: ${path}`, { cause: error });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function hookRegistrationPath(harness, scope) {
|
|
21
|
+
// ponytail: user scope resolves against AGENT_OPS_HOME, so the same
|
|
22
|
+
// relative path serves both scopes.
|
|
23
|
+
void scope;
|
|
24
|
+
return harness === "claude" ? CLAUDE_HOOK_PATH : CODEX_HOOK_PATH;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds the merged settings file for one harness, preserving foreign hooks.
|
|
28
|
+
* Returns null when the selected capabilities register no events at all.
|
|
29
|
+
*/
|
|
30
|
+
export function planHookRegistration(options) {
|
|
31
|
+
const path = hookRegistrationPath(options.harness, options.scope);
|
|
32
|
+
const managed = options.harness === "claude"
|
|
33
|
+
? buildClaudeHookSettings(options.capabilities, options.runtimePath)
|
|
34
|
+
: buildCodexHookConfig(options.capabilities);
|
|
35
|
+
const events = Object.keys(managed.hooks);
|
|
36
|
+
if (events.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const existing = parseSettings(path, options.currentSource);
|
|
40
|
+
const merged = options.harness === "claude"
|
|
41
|
+
? mergeClaudeSettings(existing, managed)
|
|
42
|
+
: mergeCodexHookConfig(existing, managed);
|
|
43
|
+
return {
|
|
44
|
+
content: format(merged),
|
|
45
|
+
record: {
|
|
46
|
+
id: `${options.harness}-hooks`,
|
|
47
|
+
path,
|
|
48
|
+
harness: options.harness,
|
|
49
|
+
events,
|
|
50
|
+
owner: "agent-ops"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function onlyManagedRemains(harness, value) {
|
|
55
|
+
const ownKeys = new Set(harness === "codex" ? ["hooks", "description"] : ["hooks"]);
|
|
56
|
+
const hooks = value.hooks;
|
|
57
|
+
return (Object.keys(value).every((key) => ownKeys.has(key)) &&
|
|
58
|
+
typeof hooks === "object" &&
|
|
59
|
+
hooks !== null &&
|
|
60
|
+
Object.keys(hooks).length === 0);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Strips owned handlers from a hook settings file. The file is removed only
|
|
64
|
+
* when nothing but agent-ops content is left, so foreign settings survive.
|
|
65
|
+
*/
|
|
66
|
+
export function planHookRemoval(record, currentSource) {
|
|
67
|
+
const existing = parseSettings(record.path, currentSource);
|
|
68
|
+
const stripped = record.harness === "claude"
|
|
69
|
+
? stripClaudeManagedHooks(existing)
|
|
70
|
+
: stripCodexManagedHooks(existing);
|
|
71
|
+
return {
|
|
72
|
+
path: record.path,
|
|
73
|
+
content: onlyManagedRemains(record.harness, stripped)
|
|
74
|
+
? null
|
|
75
|
+
: format(stripped)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
3
|
import { COMMON_AGENTS_BLOCK, COMMON_CLAUDE_BLOCK } from "./harness.js";
|
|
4
|
+
import { hookRegistrationPath } from "./hooks.js";
|
|
4
5
|
function selectedHarnesses(manifest) {
|
|
5
6
|
return manifest.harness === "both"
|
|
6
7
|
? ["codex", "claude"]
|
|
@@ -24,6 +25,24 @@ function expectedMarker(manifest, id) {
|
|
|
24
25
|
function manifestOwnershipError() {
|
|
25
26
|
return new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest does not match a supported managed installation shape.");
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Hook records are optional: installations without hook capabilities, and
|
|
30
|
+
* manifests written before hook registration existed, carry none.
|
|
31
|
+
*/
|
|
32
|
+
function assertSupportedHookRecords(manifest, harnesses) {
|
|
33
|
+
const selected = new Set(harnesses);
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const hook of manifest.hooks ?? []) {
|
|
36
|
+
if (!selected.has(hook.harness) ||
|
|
37
|
+
seen.has(hook.harness) ||
|
|
38
|
+
hook.id !== `${hook.harness}-hooks` ||
|
|
39
|
+
hook.path !== hookRegistrationPath(hook.harness, manifest.scope) ||
|
|
40
|
+
hook.events.length === 0) {
|
|
41
|
+
throw manifestOwnershipError();
|
|
42
|
+
}
|
|
43
|
+
seen.add(hook.harness);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
27
46
|
export function assertSupportedManifestOwnership(manifest) {
|
|
28
47
|
const harnesses = selectedHarnesses(manifest);
|
|
29
48
|
const expectedArtifacts = new Map([
|
|
@@ -53,6 +72,7 @@ export function assertSupportedManifestOwnership(manifest) {
|
|
|
53
72
|
throw manifestOwnershipError();
|
|
54
73
|
}
|
|
55
74
|
}
|
|
75
|
+
assertSupportedHookRecords(manifest, harnesses);
|
|
56
76
|
return expectedMarkers;
|
|
57
77
|
}
|
|
58
78
|
function exactMarkerCount(source, marker) {
|
|
@@ -5,7 +5,8 @@ import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
|
|
|
5
5
|
import { formatInstallManifest, parseInstallManifest } from "../fs/manifest.js";
|
|
6
6
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
7
7
|
import { validateConfig } from "../schema/validate.js";
|
|
8
|
-
import { planHarnessContributions } from "./harness.js";
|
|
8
|
+
import { planHarnessContributions, requestedHarnessIds } from "./harness.js";
|
|
9
|
+
import { hookRegistrationPath, planHookRegistration } from "./hooks.js";
|
|
9
10
|
import { resolveProfiles } from "./profiles.js";
|
|
10
11
|
const CONFIG_PATH = ".agent-ops/config.json";
|
|
11
12
|
const MANIFEST_PATH = ".agent-ops/manifest.json";
|
|
@@ -233,12 +234,36 @@ export async function createInstallPlan(options) {
|
|
|
233
234
|
}
|
|
234
235
|
const plannedBlocks = await planBlocks(options.root, contribution.blocks);
|
|
235
236
|
operations.push(...plannedBlocks.operations);
|
|
237
|
+
const hooks = [];
|
|
238
|
+
if (options.hookRuntimePath !== undefined) {
|
|
239
|
+
for (const harness of requestedHarnessIds(options.harness)) {
|
|
240
|
+
const current = await readCurrentFile(options.root, hookRegistrationPath(harness, options.scope));
|
|
241
|
+
const planned = planHookRegistration({
|
|
242
|
+
harness,
|
|
243
|
+
scope: options.scope,
|
|
244
|
+
capabilities: resolved.capabilities,
|
|
245
|
+
runtimePath: options.hookRuntimePath,
|
|
246
|
+
currentSource: current?.content ?? null
|
|
247
|
+
});
|
|
248
|
+
if (planned === null) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
operations.push({
|
|
252
|
+
kind: "write",
|
|
253
|
+
path: planned.record.path,
|
|
254
|
+
content: planned.content,
|
|
255
|
+
expectedHash: current?.hash ?? null
|
|
256
|
+
});
|
|
257
|
+
hooks.push(planned.record);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
236
260
|
const manifest = {
|
|
237
261
|
schemaVersion: SCHEMA_VERSION,
|
|
238
262
|
scope: options.scope,
|
|
239
263
|
harness: options.harness,
|
|
240
264
|
artifacts,
|
|
241
|
-
markers: plannedBlocks.records
|
|
265
|
+
markers: plannedBlocks.records,
|
|
266
|
+
...(hooks.length === 0 ? {} : { hooks })
|
|
242
267
|
};
|
|
243
268
|
operations.push({
|
|
244
269
|
kind: "write",
|
|
@@ -5,6 +5,7 @@ import { removeManagedBlock } from "../fs/managed-block.js";
|
|
|
5
5
|
import { parseInstallManifest } from "../fs/manifest.js";
|
|
6
6
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
7
7
|
import { FileTransaction } from "../fs/transaction.js";
|
|
8
|
+
import { planHookRemoval } from "./hooks.js";
|
|
8
9
|
import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
|
|
9
10
|
const MANIFEST_PATH = ".agent-ops/manifest.json";
|
|
10
11
|
const MAX_UNINSTALL_FILE_BYTES = 1024 * 1024;
|
|
@@ -130,6 +131,25 @@ export async function createUninstallPlan(root) {
|
|
|
130
131
|
});
|
|
131
132
|
}
|
|
132
133
|
operations.push(...await planMarkerFiles(root, manifest.markers, expectedMarkers));
|
|
134
|
+
for (const hook of manifest.hooks ?? []) {
|
|
135
|
+
const current = await readCurrentFile(root, hook.path);
|
|
136
|
+
if (current === null) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const removal = planHookRemoval(hook, current.content);
|
|
140
|
+
operations.push(removal.content === null
|
|
141
|
+
? {
|
|
142
|
+
kind: "remove",
|
|
143
|
+
path: hook.path,
|
|
144
|
+
expectedHash: current.hash
|
|
145
|
+
}
|
|
146
|
+
: {
|
|
147
|
+
kind: "write",
|
|
148
|
+
path: hook.path,
|
|
149
|
+
content: removal.content,
|
|
150
|
+
expectedHash: current.hash
|
|
151
|
+
});
|
|
152
|
+
}
|
|
133
153
|
operations.push({
|
|
134
154
|
kind: "remove",
|
|
135
155
|
path: MANIFEST_PATH,
|
|
@@ -149,6 +169,7 @@ function allowedPaths(plan) {
|
|
|
149
169
|
return new Set([
|
|
150
170
|
...plan.manifest.artifacts.map(({ path }) => path.toLowerCase()),
|
|
151
171
|
...plan.manifest.markers.map(({ path }) => path.toLowerCase()),
|
|
172
|
+
...(plan.manifest.hooks ?? []).map(({ path }) => path.toLowerCase()),
|
|
152
173
|
MANIFEST_PATH
|
|
153
174
|
]);
|
|
154
175
|
}
|
|
@@ -187,6 +208,13 @@ async function validateUninstalled(root, manifest) {
|
|
|
187
208
|
throw new AgentOpsError("UNINSTALL_VALIDATION_FAILED", `Managed block still exists: ${marker.path}`);
|
|
188
209
|
}
|
|
189
210
|
}
|
|
211
|
+
for (const hook of manifest.hooks ?? []) {
|
|
212
|
+
const current = await readCurrentFile(root, hook.path);
|
|
213
|
+
if (current !== null &&
|
|
214
|
+
planHookRemoval(hook, current.content).content !== current.content) {
|
|
215
|
+
throw new AgentOpsError("UNINSTALL_VALIDATION_FAILED", `Managed hook handlers still exist: ${hook.path}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
190
218
|
if (await readCurrentFile(root, MANIFEST_PATH) !== null) {
|
|
191
219
|
throw new AgentOpsError("UNINSTALL_VALIDATION_FAILED", "Installation manifest still exists.");
|
|
192
220
|
}
|
|
@@ -107,6 +107,9 @@ export async function createUpdatePlan(options) {
|
|
|
107
107
|
profiles: configPreview.migrated.profiles,
|
|
108
108
|
adapters: options.adapters,
|
|
109
109
|
toolkitVersion: targetVersion,
|
|
110
|
+
...(options.hookRuntimePath === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { hookRuntimePath: options.hookRuntimePath }),
|
|
110
113
|
existingConfig: {
|
|
111
114
|
value: configPreview.migrated,
|
|
112
115
|
sourceHash: configPreview.sourceHash
|
|
@@ -6,6 +6,12 @@ const PROFILE_VALUES = new Set(["advisory", "core", "guardrails"]);
|
|
|
6
6
|
const EVIDENCE_KINDS = new Set(["exit-code", "file", "test-count"]);
|
|
7
7
|
const SCOPE_VALUES = new Set(["project", "user"]);
|
|
8
8
|
const HARNESS_VALUES = new Set(["both", "claude", "codex"]);
|
|
9
|
+
const HOOK_HARNESS_VALUES = new Set(["claude", "codex"]);
|
|
10
|
+
const HOOK_EVENT_VALUES = new Set([
|
|
11
|
+
"SessionStart",
|
|
12
|
+
"PreToolUse",
|
|
13
|
+
"Stop"
|
|
14
|
+
]);
|
|
9
15
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
10
16
|
const MAX_EXIT_CODE = 4_294_967_295;
|
|
11
17
|
function failure(code, path, message) {
|
|
@@ -520,10 +526,40 @@ function validateManagedMarker(value, path) {
|
|
|
520
526
|
}
|
|
521
527
|
return success(value);
|
|
522
528
|
}
|
|
529
|
+
function validateManagedHook(value, path) {
|
|
530
|
+
if (!isRecord(value)) {
|
|
531
|
+
return failure("INVALID_TYPE", path, "Expected a managed hook record.");
|
|
532
|
+
}
|
|
533
|
+
const unknown = unknownFieldFailure(value, ["events", "harness", "id", "owner", "path"], path);
|
|
534
|
+
if (unknown !== undefined) {
|
|
535
|
+
return unknown;
|
|
536
|
+
}
|
|
537
|
+
if (!isIdentifier(value.id)) {
|
|
538
|
+
return failure("INVALID_ID", `${path}.id`, "Invalid managed entry ID.");
|
|
539
|
+
}
|
|
540
|
+
if (!isSafeRelativePath(value.path) || value.path === ".") {
|
|
541
|
+
return failure("INVALID_RELATIVE_PATH", `${path}.path`, "Managed paths must be project-relative.");
|
|
542
|
+
}
|
|
543
|
+
if (typeof value.harness !== "string" ||
|
|
544
|
+
!HOOK_HARNESS_VALUES.has(value.harness)) {
|
|
545
|
+
return failure("INVALID_HARNESS", `${path}.harness`, "Hook records must name a single harness.");
|
|
546
|
+
}
|
|
547
|
+
if (!isStringArray(value.events) ||
|
|
548
|
+
value.events.length === 0 ||
|
|
549
|
+
new Set(value.events).size !== value.events.length ||
|
|
550
|
+
value.events.some((event) => !HOOK_EVENT_VALUES.has(event))) {
|
|
551
|
+
return failure("INVALID_HOOK_EVENTS", `${path}.events`, "Hook records must list distinct supported events.");
|
|
552
|
+
}
|
|
553
|
+
if (value.owner !== "agent-ops") {
|
|
554
|
+
return failure("INVALID_OWNER", `${path}.owner`, "Managed entries must be owned by agent-ops.");
|
|
555
|
+
}
|
|
556
|
+
return success(value);
|
|
557
|
+
}
|
|
523
558
|
export function validateManifest(value) {
|
|
524
559
|
const root = validateRoot(value, [
|
|
525
560
|
"artifacts",
|
|
526
561
|
"harness",
|
|
562
|
+
"hooks",
|
|
527
563
|
"markers",
|
|
528
564
|
"schemaVersion",
|
|
529
565
|
"scope"
|
|
@@ -580,5 +616,26 @@ export function validateManifest(value) {
|
|
|
580
616
|
markerBoundaries.add(startBoundary);
|
|
581
617
|
markerBoundaries.add(endBoundary);
|
|
582
618
|
}
|
|
619
|
+
if (root.hooks !== undefined) {
|
|
620
|
+
if (!Array.isArray(root.hooks)) {
|
|
621
|
+
return failure("INVALID_TYPE", "$.hooks", "hooks must be an array.");
|
|
622
|
+
}
|
|
623
|
+
const hookPaths = new Set();
|
|
624
|
+
for (const [index, hookValue] of root.hooks.entries()) {
|
|
625
|
+
const hook = validateManagedHook(hookValue, `$.hooks[${index}]`);
|
|
626
|
+
if (!hook.ok) {
|
|
627
|
+
return hook;
|
|
628
|
+
}
|
|
629
|
+
const hookPathKey = hook.value.path.toLowerCase();
|
|
630
|
+
if (entryIds.has(hook.value.id)) {
|
|
631
|
+
return failure("DUPLICATE_ID", `$.hooks[${index}].id`, `Duplicate manifest entry ID: ${hook.value.id}`);
|
|
632
|
+
}
|
|
633
|
+
if (artifactPaths.has(hookPathKey) || hookPaths.has(hookPathKey)) {
|
|
634
|
+
return failure("DUPLICATE_OWNERSHIP", `$.hooks[${index}].path`, `Hook path is owned more than once: ${hook.value.path}`);
|
|
635
|
+
}
|
|
636
|
+
entryIds.add(hook.value.id);
|
|
637
|
+
hookPaths.add(hookPathKey);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
583
640
|
return success(root);
|
|
584
641
|
}
|
|
@@ -241,7 +241,9 @@ async function readWindowsProcessIdentity(processId) {
|
|
|
241
241
|
], {
|
|
242
242
|
encoding: "utf8",
|
|
243
243
|
maxBuffer: 4096,
|
|
244
|
-
|
|
244
|
+
// ponytail: cold PowerShell start on a loaded machine routinely
|
|
245
|
+
// passes two seconds; the lock budget above still bounds the wait.
|
|
246
|
+
timeout: 10_000,
|
|
245
247
|
windowsHide: true
|
|
246
248
|
});
|
|
247
249
|
const startedAt = result.stdout.trim();
|
|
@@ -251,10 +253,27 @@ async function readWindowsProcessIdentity(processId) {
|
|
|
251
253
|
return null;
|
|
252
254
|
}
|
|
253
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* A live process cannot change its own start time, so a successful self
|
|
258
|
+
* lookup never expires. Every other entry keeps the short TTL: foreign
|
|
259
|
+
* process IDs get recycled, and a failed self lookup must stay retryable
|
|
260
|
+
* instead of pinning the fallback identity for the whole run.
|
|
261
|
+
*/
|
|
262
|
+
export function reuseCachedProcessIdentity(entry, processId, selfProcessId, now) {
|
|
263
|
+
if (entry === undefined) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (processId === selfProcessId &&
|
|
267
|
+
entry.identity !== null &&
|
|
268
|
+
!entry.identity.startsWith("runtime:")) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return now - entry.checkedAt <= PROCESS_IDENTITY_CACHE_MS;
|
|
272
|
+
}
|
|
254
273
|
async function readProcessIdentity(processId) {
|
|
255
274
|
const cached = processIdentityCache.get(processId);
|
|
256
|
-
if (cached
|
|
257
|
-
|
|
275
|
+
if (reuseCachedProcessIdentity(cached, processId, process.pid, Date.now()) &&
|
|
276
|
+
cached !== undefined) {
|
|
258
277
|
return cached.identity;
|
|
259
278
|
}
|
|
260
279
|
let identity;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kylecheng3146/agent-ops",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Evidence-driven development loops for Codex and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"docs/zh-TW/spec/",
|
|
20
20
|
"README.md",
|
|
21
21
|
"LICENSE",
|
|
22
|
-
"SECURITY.md"
|
|
22
|
+
"SECURITY.md",
|
|
23
|
+
"postinstall.cjs"
|
|
23
24
|
],
|
|
24
25
|
"publishConfig": {
|
|
25
26
|
"access": "public"
|
|
@@ -30,7 +31,8 @@
|
|
|
30
31
|
"test:compile": "node scripts/clean.mjs .tmp && tsc -p tsconfig.test.json",
|
|
31
32
|
"test": "npm run test:compile && node scripts/run-tests.mjs .tmp/test-dist/tests",
|
|
32
33
|
"build": "node scripts/clean.mjs dist && tsc -p tsconfig.build.json",
|
|
33
|
-
"package:check": "node scripts/package-check.mjs"
|
|
34
|
+
"package:check": "node scripts/package-check.mjs",
|
|
35
|
+
"postinstall": "node postinstall.cjs"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@types/node": "26.1.1",
|
package/postinstall.cjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { closeSync, existsSync, openSync, readFileSync } = require("node:fs");
|
|
4
|
+
const { join, resolve } = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const { isatty } = require("node:tty");
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = "@kylecheng3146/agent-ops";
|
|
9
|
+
const packageRoot = __dirname;
|
|
10
|
+
const installRoot = resolve(process.env.INIT_CWD ?? process.cwd());
|
|
11
|
+
|
|
12
|
+
function isDirectDependency() {
|
|
13
|
+
try {
|
|
14
|
+
const packageJson = JSON.parse(
|
|
15
|
+
readFileSync(join(installRoot, "package.json"), "utf8")
|
|
16
|
+
);
|
|
17
|
+
return [
|
|
18
|
+
packageJson.dependencies,
|
|
19
|
+
packageJson.devDependencies,
|
|
20
|
+
packageJson.optionalDependencies,
|
|
21
|
+
packageJson.peerDependencies
|
|
22
|
+
].some(
|
|
23
|
+
(dependencies) =>
|
|
24
|
+
dependencies !== null &&
|
|
25
|
+
typeof dependencies === "object" &&
|
|
26
|
+
dependencies[PACKAGE_NAME] !== undefined
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openTerminal() {
|
|
34
|
+
if (process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
35
|
+
return { stdio: "inherit", close() {} };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const inputPath = process.platform === "win32" ? "CONIN$" : "/dev/tty";
|
|
39
|
+
const outputPath = process.platform === "win32" ? "CONOUT$" : "/dev/tty";
|
|
40
|
+
let inputFd;
|
|
41
|
+
let outputFd;
|
|
42
|
+
try {
|
|
43
|
+
inputFd = openSync(inputPath, "r");
|
|
44
|
+
outputFd = process.platform === "win32" ? openSync(outputPath, "a") : inputFd;
|
|
45
|
+
if (!isatty(inputFd) || !isatty(outputFd)) {
|
|
46
|
+
throw new Error("Interactive terminal is unavailable.");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
stdio: [inputFd, outputFd, outputFd],
|
|
50
|
+
close() {
|
|
51
|
+
if (outputFd !== inputFd) {
|
|
52
|
+
closeSync(outputFd);
|
|
53
|
+
}
|
|
54
|
+
closeSync(inputFd);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
if (outputFd !== undefined && outputFd !== inputFd) {
|
|
59
|
+
closeSync(outputFd);
|
|
60
|
+
}
|
|
61
|
+
if (inputFd !== undefined) {
|
|
62
|
+
closeSync(inputFd);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
installRoot === resolve(packageRoot) ||
|
|
70
|
+
process.env.CI !== undefined ||
|
|
71
|
+
!isDirectDependency()
|
|
72
|
+
) {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const terminal = openTerminal();
|
|
77
|
+
if (terminal === null) {
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cliPath = join(packageRoot, "dist", "packages", "cli", "src", "bin.js");
|
|
82
|
+
if (!existsSync(cliPath)) {
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
"agent-ops init was skipped because the installed CLI is unavailable.\n"
|
|
85
|
+
);
|
|
86
|
+
terminal.close();
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = spawnSync(process.execPath, [cliPath], {
|
|
91
|
+
cwd: installRoot,
|
|
92
|
+
env: process.env,
|
|
93
|
+
stdio: terminal.stdio
|
|
94
|
+
});
|
|
95
|
+
terminal.close();
|
|
96
|
+
|
|
97
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
"agent-ops init was not applied; package installation completed. " +
|
|
100
|
+
"Run `agent-ops` to retry.\n"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
"items": {
|
|
27
27
|
"$ref": "#/$defs/managedMarker"
|
|
28
28
|
}
|
|
29
|
+
},
|
|
30
|
+
"hooks": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": {
|
|
33
|
+
"$ref": "#/$defs/managedHook"
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
},
|
|
31
37
|
"$defs": {
|
|
@@ -104,6 +110,33 @@
|
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
},
|
|
113
|
+
"managedHook": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"additionalProperties": false,
|
|
116
|
+
"required": ["id", "path", "harness", "events", "owner"],
|
|
117
|
+
"properties": {
|
|
118
|
+
"id": {
|
|
119
|
+
"$ref": "#/$defs/id"
|
|
120
|
+
},
|
|
121
|
+
"path": {
|
|
122
|
+
"$ref": "#/$defs/relativePath"
|
|
123
|
+
},
|
|
124
|
+
"harness": {
|
|
125
|
+
"enum": ["codex", "claude"]
|
|
126
|
+
},
|
|
127
|
+
"events": {
|
|
128
|
+
"type": "array",
|
|
129
|
+
"minItems": 1,
|
|
130
|
+
"uniqueItems": true,
|
|
131
|
+
"items": {
|
|
132
|
+
"enum": ["SessionStart", "PreToolUse", "Stop"]
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"owner": {
|
|
136
|
+
"const": "agent-ops"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
107
140
|
"nonEmptyString": {
|
|
108
141
|
"type": "string",
|
|
109
142
|
"pattern": "[^\\s\\u0000]",
|