@agentex/agent 0.0.36 → 0.0.38
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/CHANGELOG.md +460 -0
- package/README.md +32 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts +3 -0
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +23 -1
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/claude/session.d.ts +174 -4
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +541 -29
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +3 -0
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/parse.d.ts +12 -1
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +21 -0
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +21 -1
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/instructions.d.ts +36 -12
- package/dist/utils/instructions.d.ts.map +1 -1
- package/dist/utils/instructions.js +104 -53
- package/dist/utils/instructions.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/providers/claude/parse.ts +23 -1
- package/src/providers/claude/session.ts +556 -30
- package/src/providers/codex/execute.ts +3 -0
- package/src/providers/codex/parse.ts +25 -0
- package/src/providers/codex/session.ts +24 -1
- package/src/types.ts +97 -0
- package/src/utils/instructions.ts +152 -64
|
@@ -250,6 +250,9 @@ export async function executeCodexProvider(ctx: ExecutionContext): Promise<Execu
|
|
|
250
250
|
status: "stopped",
|
|
251
251
|
description: task.description,
|
|
252
252
|
summary: null,
|
|
253
|
+
// Cut short, so there is no result to hand back.
|
|
254
|
+
toolUseId: null,
|
|
255
|
+
report: null,
|
|
253
256
|
parentTaskId: task.parentTaskId,
|
|
254
257
|
timestamp: new Date().toISOString(),
|
|
255
258
|
providerType: "codex",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BackgroundTaskReport,
|
|
2
3
|
BaseStreamEventFields,
|
|
3
4
|
GoalSource,
|
|
4
5
|
StreamEvent,
|
|
@@ -70,6 +71,26 @@ function parseStringArray(value: unknown): string[] {
|
|
|
70
71
|
: [];
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The delivered-result payload for a background task, or null.
|
|
76
|
+
*
|
|
77
|
+
* One rule for every Codex emitter: a child that reached a terminal outcome
|
|
78
|
+
* handed something back; one that was cut short did not. Defined here rather
|
|
79
|
+
* than at each construction site because there are five of them and four
|
|
80
|
+
* previously forgot, so a host keying on `report !== null` — which the README
|
|
81
|
+
* tells it to do — rendered no row for completions that reached it by the
|
|
82
|
+
* reconcile path. Mirrors the Claude gate in `claude/parse.ts`.
|
|
83
|
+
*/
|
|
84
|
+
export function codexBackgroundTaskReport(
|
|
85
|
+
phase: "started" | "progress" | "completed",
|
|
86
|
+
status: "pending" | "running" | "paused" | "completed" | "failed" | "stopped" | null,
|
|
87
|
+
summary: string | null,
|
|
88
|
+
): BackgroundTaskReport | null {
|
|
89
|
+
if (phase !== "completed") return null;
|
|
90
|
+
if (status !== "completed" && status !== "failed") return null;
|
|
91
|
+
return { summary, outputFile: null, usage: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
function codexBackgroundTaskState(
|
|
74
95
|
value: unknown,
|
|
75
96
|
): { phase: "started" | "completed"; status: "running" | "completed" | "failed" | "stopped"; summary: string | null } {
|
|
@@ -454,6 +475,8 @@ function parseV2Notification(event: Record<string, unknown>): StreamEvent | Stre
|
|
|
454
475
|
description: asNullableString(item["agentPath"]),
|
|
455
476
|
summary: null,
|
|
456
477
|
parentTaskId: null,
|
|
478
|
+
toolUseId: null,
|
|
479
|
+
report: null,
|
|
457
480
|
...base,
|
|
458
481
|
};
|
|
459
482
|
}
|
|
@@ -491,6 +514,8 @@ function parseV2Notification(event: Record<string, unknown>): StreamEvent | Stre
|
|
|
491
514
|
description,
|
|
492
515
|
summary: state.summary,
|
|
493
516
|
parentTaskId: null,
|
|
517
|
+
toolUseId: null,
|
|
518
|
+
report: codexBackgroundTaskReport(state.phase, state.status, state.summary),
|
|
494
519
|
...base,
|
|
495
520
|
};
|
|
496
521
|
});
|
|
@@ -2,6 +2,7 @@ import type { ChildProcess } from "node:child_process";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type {
|
|
5
|
+
BackgroundTaskReport,
|
|
5
6
|
AgentSession,
|
|
6
7
|
CancelResult,
|
|
7
8
|
ClearGoalResult,
|
|
@@ -27,7 +28,7 @@ import { translateEndpoint } from "../../utils/endpoint.js";
|
|
|
27
28
|
import { injectWorkspaceSkills } from "../../utils/skills.js";
|
|
28
29
|
import { resolveInstructions } from "../../utils/instructions.js";
|
|
29
30
|
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
30
|
-
import { parseCodexStreamLines } from "./parse.js";
|
|
31
|
+
import { parseCodexStreamLines, codexBackgroundTaskReport } from "./parse.js";
|
|
31
32
|
import { withPlanModePreamble } from "./plan-mode.js";
|
|
32
33
|
import { scanCodexSessionUsage } from "./usage-scanner.js";
|
|
33
34
|
import { codexSessionCodec } from "./codec.js";
|
|
@@ -1448,6 +1449,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1448
1449
|
parentTaskId?: string | null;
|
|
1449
1450
|
turnId?: string | null;
|
|
1450
1451
|
eventId?: string | null;
|
|
1452
|
+
/** Set on the edge that actually hands the child's result back. */
|
|
1453
|
+
report?: BackgroundTaskReport | null;
|
|
1451
1454
|
raw: Record<string, unknown>;
|
|
1452
1455
|
},
|
|
1453
1456
|
): Extract<StreamEvent, { type: "background_task" }> {
|
|
@@ -1461,6 +1464,16 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1461
1464
|
description: options.description ?? null,
|
|
1462
1465
|
summary: options.summary ?? null,
|
|
1463
1466
|
parentTaskId: options.parentTaskId ?? null,
|
|
1467
|
+
// Codex identifies children by thread id, not by the id of the
|
|
1468
|
+
// `spawn_agent` call that created them, so there is no tool-call link to
|
|
1469
|
+
// report here.
|
|
1470
|
+
toolUseId: null,
|
|
1471
|
+
// Derived, not passed. Four of the five emitters that reach this helper
|
|
1472
|
+
// never set it, and the omission was invisible until a host asked why
|
|
1473
|
+
// reconciled completions produced no row.
|
|
1474
|
+
report: options.report !== undefined
|
|
1475
|
+
? options.report
|
|
1476
|
+
: codexBackgroundTaskReport(phase, status, options.summary ?? null),
|
|
1464
1477
|
timestamp: new Date().toISOString(),
|
|
1465
1478
|
providerType: "codex",
|
|
1466
1479
|
sessionId: rootThreadId,
|
|
@@ -1833,6 +1846,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1833
1846
|
status: "running",
|
|
1834
1847
|
description,
|
|
1835
1848
|
summary: null,
|
|
1849
|
+
toolUseId: null,
|
|
1850
|
+
report: null,
|
|
1836
1851
|
parentTaskId: parentThreadId === rootThreadId ? null : parentThreadId,
|
|
1837
1852
|
timestamp: new Date().toISOString(),
|
|
1838
1853
|
providerType: "codex",
|
|
@@ -1867,6 +1882,8 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1867
1882
|
status: "running",
|
|
1868
1883
|
description: task.description,
|
|
1869
1884
|
summary: null,
|
|
1885
|
+
toolUseId: null,
|
|
1886
|
+
report: null,
|
|
1870
1887
|
parentTaskId: task.parentTaskId,
|
|
1871
1888
|
timestamp: new Date().toISOString(),
|
|
1872
1889
|
providerType: "codex",
|
|
@@ -1920,6 +1937,12 @@ export class CodexSessionImpl implements AgentSession {
|
|
|
1920
1937
|
description: task.description,
|
|
1921
1938
|
summary: task.summary ?? (errorMessage || null),
|
|
1922
1939
|
parentTaskId: task.parentTaskId,
|
|
1940
|
+
toolUseId: null,
|
|
1941
|
+
// The child's turn ended and its result is being handed back — the same
|
|
1942
|
+
// edge Claude expresses as `task_notification`. Same rule as every other
|
|
1943
|
+
// emitter, so a host can render "one row per delivered result" across
|
|
1944
|
+
// providers instead of special-casing each one.
|
|
1945
|
+
report: codexBackgroundTaskReport("completed", status, task.summary ?? (errorMessage || null)),
|
|
1923
1946
|
timestamp: new Date().toISOString(),
|
|
1924
1947
|
providerType: "codex",
|
|
1925
1948
|
sessionId: rootThreadId,
|
package/src/types.ts
CHANGED
|
@@ -998,6 +998,48 @@ export type BackgroundTaskType = "subagent" | "process" | "unknown";
|
|
|
998
998
|
/** Lifecycle edge represented by one `background_task` StreamEvent. */
|
|
999
999
|
export type BackgroundTaskPhase = "started" | "progress" | "completed";
|
|
1000
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* A background task's delivered output.
|
|
1003
|
+
*
|
|
1004
|
+
* Present only on the event where the provider actually *hands the result
|
|
1005
|
+
* back* — distinct from a state patch that merely says the task reached a
|
|
1006
|
+
* terminal status. Claude emits both for one completion (`task_updated` then
|
|
1007
|
+
* `task_notification`), and they are different records, not duplicates: only
|
|
1008
|
+
* the delivery carries the summary, the output file, and the `toolUseId`
|
|
1009
|
+
* linking the task to the call that launched it.
|
|
1010
|
+
*
|
|
1011
|
+
* Collapsing the two into one indistinguishable "completed" event is what
|
|
1012
|
+
* made hosts render every finished task twice: once with its report and once
|
|
1013
|
+
* with nothing. Only the delivery carries the summary and the output file
|
|
1014
|
+
* (`toolUseId` is also on the task's `started` record).
|
|
1015
|
+
*
|
|
1016
|
+
* Absent on a task that was cut short — a stop or a kill delivers no result.
|
|
1017
|
+
*/
|
|
1018
|
+
export interface BackgroundTaskReport {
|
|
1019
|
+
/** The task's final output as the provider summarized it. */
|
|
1020
|
+
summary: string | null;
|
|
1021
|
+
/** Path to the task's full transcript/output on disk, when the provider writes one. */
|
|
1022
|
+
outputFile: string | null;
|
|
1023
|
+
/** What the task consumed, when reported. */
|
|
1024
|
+
usage: {
|
|
1025
|
+
totalTokens: number | null;
|
|
1026
|
+
toolUses: number | null;
|
|
1027
|
+
durationMs: number | null;
|
|
1028
|
+
} | null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Why a turn began.
|
|
1033
|
+
*
|
|
1034
|
+
* - `send` — dispatched through `send()`. Usually the host; agentex's own
|
|
1035
|
+
* emulated goal loop also continues a session this way, so this means
|
|
1036
|
+
* "someone called send()", not strictly "the user".
|
|
1037
|
+
* - `resume` — the provider started it on its own. Claude does this when a
|
|
1038
|
+
* background task finishes: it enqueues a task-notification as user input,
|
|
1039
|
+
* which opens a fresh turn with no host involvement.
|
|
1040
|
+
*/
|
|
1041
|
+
export type TurnTrigger = "send" | "resume";
|
|
1042
|
+
|
|
1001
1043
|
/** Current normalized state carried by a `background_task` event. */
|
|
1002
1044
|
export type BackgroundTaskStatus =
|
|
1003
1045
|
| "pending"
|
|
@@ -1229,6 +1271,61 @@ export type StreamEvent =
|
|
|
1229
1271
|
description: string | null;
|
|
1230
1272
|
summary: string | null;
|
|
1231
1273
|
parentTaskId: string | null;
|
|
1274
|
+
/**
|
|
1275
|
+
* The `tool_call` id that launched this task, when the provider reports
|
|
1276
|
+
* it. This is the structured link between a task and the tool call it
|
|
1277
|
+
* came from — the same id Claude writes into a subagent's `meta.json`.
|
|
1278
|
+
*/
|
|
1279
|
+
toolUseId: string | null;
|
|
1280
|
+
/**
|
|
1281
|
+
* The task's delivered output, on the event that delivers it, else null.
|
|
1282
|
+
* A terminal `status` says the task finished; a non-null `report` says
|
|
1283
|
+
* its result is being handed back. See `BackgroundTaskReport`.
|
|
1284
|
+
*/
|
|
1285
|
+
report: BackgroundTaskReport | null;
|
|
1286
|
+
} & BaseStreamEventFields)
|
|
1287
|
+
/**
|
|
1288
|
+
* A turn opened. Pairs with `result`, which closes one.
|
|
1289
|
+
*
|
|
1290
|
+
* Hosts that track "is the agent working" cannot derive it from their own
|
|
1291
|
+
* dispatch alone, because not every turn is theirs: when a background task
|
|
1292
|
+
* finishes, Claude enqueues a notification as user input and starts a turn
|
|
1293
|
+
* by itself. A host keying off its own `send()` sees that turn as idle and
|
|
1294
|
+
* reports the session as finished while it is visibly working.
|
|
1295
|
+
*
|
|
1296
|
+
* Emitted for host-initiated turns too, so `turn_start` → `result` describes
|
|
1297
|
+
* turn liveness straight off the stream.
|
|
1298
|
+
*
|
|
1299
|
+
* Deliberately does not name the background task behind a `resume`. Claude
|
|
1300
|
+
* delivers a task's result and opens the turn as two unlinked records, and
|
|
1301
|
+
* with several tasks in flight the pairing is not recoverable from the wire
|
|
1302
|
+
* — every attempt to infer it produced a plausible id that was sometimes
|
|
1303
|
+
* simply wrong. Correlate through `background_task.report` and `toolUseId`,
|
|
1304
|
+
* which the provider does state.
|
|
1305
|
+
*/
|
|
1306
|
+
| ({
|
|
1307
|
+
type: "turn_start";
|
|
1308
|
+
turnId: string;
|
|
1309
|
+
trigger: TurnTrigger;
|
|
1310
|
+
} & BaseStreamEventFields)
|
|
1311
|
+
/**
|
|
1312
|
+
* A turn closed. Every `turn_start` is followed by exactly one of these.
|
|
1313
|
+
*
|
|
1314
|
+
* `result` cannot serve as the close signal on its own: a message the CLI
|
|
1315
|
+
* cancels, discards, or refuses opens a turn and produces no result at all,
|
|
1316
|
+
* so a host tracking `turn_start` → `result` would stay busy forever. This
|
|
1317
|
+
* is the guaranteed counterpart; `result` remains the outcome payload.
|
|
1318
|
+
*/
|
|
1319
|
+
| ({
|
|
1320
|
+
type: "turn_end";
|
|
1321
|
+
turnId: string;
|
|
1322
|
+
trigger: TurnTrigger;
|
|
1323
|
+
/**
|
|
1324
|
+
* Why the turn ended. `result` means a normal completion whose payload
|
|
1325
|
+
* arrives as the accompanying `result` event; the rest are CLI verdicts
|
|
1326
|
+
* on the message that produced no result.
|
|
1327
|
+
*/
|
|
1328
|
+
reason: "result" | "cancelled" | "discarded" | "refused" | "session_closed";
|
|
1232
1329
|
} & BaseStreamEventFields)
|
|
1233
1330
|
| ({
|
|
1234
1331
|
type: "result";
|
|
@@ -29,18 +29,27 @@ export async function resolveInstructions(filePath?: string): Promise<string | n
|
|
|
29
29
|
// (.agents/skills + .claude/skills) with a per-runtime "native" escape hatch.
|
|
30
30
|
// Instruction files follow the same shape:
|
|
31
31
|
//
|
|
32
|
-
// - every runtime
|
|
33
|
-
//
|
|
34
|
-
//
|
|
32
|
+
// - every runtime reads AGENTS.md at a workspace root (Claude Code since
|
|
33
|
+
// 2.1.277, where the folder has no CLAUDE.md)
|
|
34
|
+
// - Claude's CLAUDE.md and Gemini's GEMINI.md are native files, written only
|
|
35
|
+
// on opt-in (`includeNativeFiles`)
|
|
35
36
|
//
|
|
36
37
|
// Two locations, mirroring installSkills:
|
|
37
38
|
//
|
|
38
39
|
// - "workspace": files at {cwd}/ — the repo-root AGENTS.md convention. Files
|
|
39
|
-
// dedupe by name, so the default writes
|
|
40
|
+
// dedupe by name, so the default writes AGENTS.md once.
|
|
40
41
|
// - "global": each runtime reads its own file in its own home dir
|
|
41
42
|
// (~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md, ...). There
|
|
42
43
|
// is no universal ~/AGENTS.md, so global is inherently per-runtime.
|
|
43
44
|
//
|
|
45
|
+
// Claude Code reads AGENTS.md only where no CLAUDE.md exists: a CLAUDE.md (or
|
|
46
|
+
// a CLAUDE.local.md, or one in a parent directory) hides AGENTS.md completely.
|
|
47
|
+
// So a workspace CLAUDE.md is only ever a one-line pointer (`@AGENTS.md`),
|
|
48
|
+
// never a second copy of the brief, and a CLAUDE.md already on disk is
|
|
49
|
+
// reconciled on every install: one holding nothing but our managed region is
|
|
50
|
+
// removed when the opt-in is off, and one the user wrote gets the pointer so
|
|
51
|
+
// the brief still reaches Claude.
|
|
52
|
+
//
|
|
44
53
|
// Unlike skills (which are symlinked dirs), instruction files carry content, so
|
|
45
54
|
// installInstructions does a managed-region merge: it wraps `content` in marker
|
|
46
55
|
// comments and replaces only that region on re-install, preserving anything the
|
|
@@ -48,21 +57,28 @@ export async function resolveInstructions(filePath?: string): Promise<string | n
|
|
|
48
57
|
// ===========================================================================
|
|
49
58
|
|
|
50
59
|
interface RuntimeInstructionSpec {
|
|
51
|
-
/** File this runtime reads at a workspace/repo root. AGENTS.md for
|
|
60
|
+
/** File this runtime reads at a workspace/repo root. AGENTS.md for every runtime. */
|
|
52
61
|
projectFile: string;
|
|
53
|
-
/** The runtime's own preferred filename.
|
|
62
|
+
/** The runtime's own preferred filename. Written in a workspace only on opt-in (`includeNativeFiles`). */
|
|
54
63
|
nativeFile: string;
|
|
64
|
+
/**
|
|
65
|
+
* How an opt-in native file carries the brief. "pointer": only an import of
|
|
66
|
+
* the project file (`@AGENTS.md`), because the native file's mere presence
|
|
67
|
+
* hides the project file from the runtime (Claude). "copy": the brief itself
|
|
68
|
+
* (Gemini, which doesn't read AGENTS.md by default).
|
|
69
|
+
*/
|
|
70
|
+
nativeFileMode: "pointer" | "copy";
|
|
55
71
|
/** Whether the runtime has a file-based global config. False for Cursor (global = app User Rules). */
|
|
56
72
|
hasGlobalFile: boolean;
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
const RUNTIME_INSTRUCTIONS: Record<SkillRuntime, RuntimeInstructionSpec> = {
|
|
60
|
-
claude: { projectFile: "
|
|
61
|
-
codex: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", hasGlobalFile: true },
|
|
62
|
-
opencode: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", hasGlobalFile: true },
|
|
63
|
-
gemini: { projectFile: "AGENTS.md", nativeFile: "GEMINI.md", hasGlobalFile: true },
|
|
64
|
-
cursor: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", hasGlobalFile: false },
|
|
65
|
-
pi: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", hasGlobalFile: true },
|
|
76
|
+
claude: { projectFile: "AGENTS.md", nativeFile: "CLAUDE.md", nativeFileMode: "pointer", hasGlobalFile: true },
|
|
77
|
+
codex: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
78
|
+
opencode: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
79
|
+
gemini: { projectFile: "AGENTS.md", nativeFile: "GEMINI.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
80
|
+
cursor: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: false },
|
|
81
|
+
pi: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
|
|
66
82
|
};
|
|
67
83
|
|
|
68
84
|
const ALL_RUNTIMES: SkillRuntime[] = ["claude", "codex", "gemini", "cursor", "opencode", "pi"];
|
|
@@ -78,15 +94,20 @@ export interface InstallInstructionsOptions {
|
|
|
78
94
|
cwd?: string;
|
|
79
95
|
/**
|
|
80
96
|
* Also write each runtime's native file when it differs from the shared
|
|
81
|
-
*
|
|
82
|
-
*
|
|
97
|
+
* AGENTS.md: Claude's CLAUDE.md (as a one-line `@AGENTS.md` pointer) and
|
|
98
|
+
* Gemini's GEMINI.md (as a copy). Only affects "workspace"; "global" always
|
|
99
|
+
* uses native files. Default: false.
|
|
100
|
+
*
|
|
101
|
+
* Opt in for Claude Code before 2.1.277, or on Bedrock, Vertex or Foundry,
|
|
102
|
+
* where Claude doesn't read AGENTS.md on its own.
|
|
83
103
|
*/
|
|
84
104
|
includeNativeFiles?: boolean;
|
|
85
105
|
/**
|
|
86
106
|
* Wrap `content` in managed markers and merge into any existing file,
|
|
87
107
|
* replacing only the previously-managed region and preserving everything the
|
|
88
108
|
* user wrote outside it. Default: true. When false, the file is overwritten
|
|
89
|
-
* with raw `content` (escape hatch for fully-owned files)
|
|
109
|
+
* with raw `content` (escape hatch for fully-owned files), and an existing
|
|
110
|
+
* CLAUDE.md is left alone even though it hides AGENTS.md from Claude Code.
|
|
90
111
|
*/
|
|
91
112
|
managed?: boolean;
|
|
92
113
|
/** Marker tag, so the comment reads `<!-- <tag>:managed:start -->`. Default: "agentex". */
|
|
@@ -98,7 +119,11 @@ export interface InstallInstructionsOptions {
|
|
|
98
119
|
homeDir?: string;
|
|
99
120
|
}
|
|
100
121
|
|
|
101
|
-
|
|
122
|
+
/**
|
|
123
|
+
* "removed": a workspace CLAUDE.md that held nothing but this installer's
|
|
124
|
+
* managed region, deleted because the opt-in (`includeNativeFiles`) is off.
|
|
125
|
+
*/
|
|
126
|
+
export type InstructionStatus = "created" | "updated" | "skipped" | "removed" | "error";
|
|
102
127
|
|
|
103
128
|
export interface InstructionInstallEntry {
|
|
104
129
|
/** The filename written, e.g. "AGENTS.md", "CLAUDE.md", "GEMINI.md". */
|
|
@@ -107,6 +132,8 @@ export interface InstructionInstallEntry {
|
|
|
107
132
|
targetPath: string;
|
|
108
133
|
/** Which requested runtimes this file serves. */
|
|
109
134
|
runtimes: SkillRuntime[];
|
|
135
|
+
/** Set when the file is a pointer: its managed region is `@<importsFile>`, not the brief. */
|
|
136
|
+
importsFile?: string;
|
|
110
137
|
status: InstructionStatus;
|
|
111
138
|
error?: string;
|
|
112
139
|
}
|
|
@@ -116,6 +143,7 @@ export interface InstructionInstallResult {
|
|
|
116
143
|
installed: number; // newly created files
|
|
117
144
|
updated: number; // existing files whose content changed
|
|
118
145
|
skipped: number; // content already current → no write
|
|
146
|
+
removed: number; // managed-only CLAUDE.md deleted because the opt-in is off
|
|
119
147
|
errors: number;
|
|
120
148
|
}
|
|
121
149
|
|
|
@@ -123,6 +151,8 @@ export interface InstructionTarget {
|
|
|
123
151
|
filename: string;
|
|
124
152
|
targetPath: string;
|
|
125
153
|
runtimes: SkillRuntime[];
|
|
154
|
+
/** Set when the file is a pointer: its managed region is `@<importsFile>`, not the brief. */
|
|
155
|
+
importsFile?: string;
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
export interface RemoveInstructionsOptions {
|
|
@@ -163,7 +193,8 @@ export interface ManagedBlockOptions {
|
|
|
163
193
|
* without touching disk.
|
|
164
194
|
*
|
|
165
195
|
* - "workspace": files dedupe by name under {cwd}/ (the default writes
|
|
166
|
-
*
|
|
196
|
+
* AGENTS.md once). `includeNativeFiles` adds CLAUDE.md (a pointer, marked by
|
|
197
|
+
* `importsFile`) and GEMINI.md.
|
|
167
198
|
* - "global": one file per runtime in each runtime's home dir. Runtimes without
|
|
168
199
|
* a file-based global config (Cursor) are omitted.
|
|
169
200
|
*/
|
|
@@ -181,26 +212,27 @@ export function resolveInstructionTargets(options?: {
|
|
|
181
212
|
const cwd = options?.cwd;
|
|
182
213
|
if (!cwd) throw new Error("cwd is required when location is 'workspace'");
|
|
183
214
|
|
|
184
|
-
const byFile = new Map<string,
|
|
185
|
-
const add = (filename: string, runtime: SkillRuntime) => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
215
|
+
const byFile = new Map<string, InstructionTarget>();
|
|
216
|
+
const add = (filename: string, runtime: SkillRuntime, importsFile?: string) => {
|
|
217
|
+
const target: InstructionTarget = byFile.get(filename) ?? {
|
|
218
|
+
filename,
|
|
219
|
+
targetPath: path.join(cwd, filename),
|
|
220
|
+
runtimes: [],
|
|
221
|
+
...(importsFile !== undefined && { importsFile }),
|
|
222
|
+
};
|
|
223
|
+
target.runtimes.push(runtime);
|
|
224
|
+
byFile.set(filename, target);
|
|
189
225
|
};
|
|
190
226
|
|
|
191
227
|
for (const runtime of runtimes) {
|
|
192
228
|
const spec = RUNTIME_INSTRUCTIONS[runtime];
|
|
193
229
|
add(spec.projectFile, runtime);
|
|
194
230
|
if (options?.includeNativeFiles && spec.nativeFile !== spec.projectFile) {
|
|
195
|
-
add(spec.nativeFile, runtime);
|
|
231
|
+
add(spec.nativeFile, runtime, spec.nativeFileMode === "pointer" ? spec.projectFile : undefined);
|
|
196
232
|
}
|
|
197
233
|
}
|
|
198
234
|
|
|
199
|
-
return [...byFile.
|
|
200
|
-
filename,
|
|
201
|
-
targetPath: path.join(cwd, filename),
|
|
202
|
-
runtimes: dedupeRuntimes(rts),
|
|
203
|
-
}));
|
|
235
|
+
return [...byFile.values()].map((target) => ({ ...target, runtimes: dedupeRuntimes(target.runtimes) }));
|
|
204
236
|
}
|
|
205
237
|
|
|
206
238
|
// global: each runtime reads its own native file in its own home dir.
|
|
@@ -229,18 +261,25 @@ function dedupeRuntimes(runtimes: SkillRuntime[]): SkillRuntime[] {
|
|
|
229
261
|
/**
|
|
230
262
|
* Install an instruction brief into the right per-runtime files.
|
|
231
263
|
*
|
|
232
|
-
* By default merges `content` into a managed region of `{cwd}/
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
264
|
+
* By default merges `content` into a managed region of `{cwd}/AGENTS.md`,
|
|
265
|
+
* preserving any user-authored content outside the markers. Idempotent: a
|
|
266
|
+
* re-install with unchanged content reports every entry as "skipped".
|
|
267
|
+
*
|
|
268
|
+
* When Claude is among the runtimes, a `{cwd}/CLAUDE.md` already on disk is
|
|
269
|
+
* reconciled, because it would hide AGENTS.md from Claude Code: one holding
|
|
270
|
+
* nothing but this installer's managed region is removed, and one the user
|
|
271
|
+
* wrote gets `@AGENTS.md` in its managed region. With `includeNativeFiles`,
|
|
272
|
+
* CLAUDE.md is written as that pointer either way. `managed: false` leaves an
|
|
273
|
+
* existing CLAUDE.md alone.
|
|
236
274
|
*
|
|
237
275
|
* @example
|
|
238
276
|
* ```ts
|
|
239
277
|
* // Workspace (repo-root) install — the common case.
|
|
240
278
|
* await installInstructions(brief, { location: "workspace", cwd: projectDir });
|
|
241
|
-
* // → {cwd}/
|
|
279
|
+
* // → {cwd}/AGENTS.md
|
|
242
280
|
*
|
|
243
|
-
* // Also
|
|
281
|
+
* // Also write the native files: CLAUDE.md as an `@AGENTS.md` pointer (for
|
|
282
|
+
* // Claude Code before 2.1.277, or on Bedrock/Vertex/Foundry) and GEMINI.md.
|
|
244
283
|
* await installInstructions(brief, { location: "workspace", cwd, includeNativeFiles: true });
|
|
245
284
|
*
|
|
246
285
|
* // Global install — per-runtime home files.
|
|
@@ -254,37 +293,16 @@ export async function installInstructions(
|
|
|
254
293
|
): Promise<InstructionInstallResult> {
|
|
255
294
|
const managed = options?.managed ?? true;
|
|
256
295
|
const tag = options?.managedTag ?? DEFAULT_MANAGED_TAG;
|
|
257
|
-
const targets = resolveInstructionTargets(options);
|
|
258
296
|
const entries: InstructionInstallEntry[] = [];
|
|
259
297
|
|
|
260
|
-
for (const target of
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (existing === null) {
|
|
269
|
-
status = "created";
|
|
270
|
-
} else if (existing === next) {
|
|
271
|
-
status = "skipped";
|
|
272
|
-
} else {
|
|
273
|
-
status = "updated";
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (status !== "skipped") {
|
|
277
|
-
await fs.mkdir(path.dirname(target.targetPath), { recursive: true });
|
|
278
|
-
await fs.writeFile(target.targetPath, next, { mode: 0o644 });
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
entries.push({ ...target, status });
|
|
282
|
-
} catch (err) {
|
|
283
|
-
entries.push({
|
|
284
|
-
...target,
|
|
285
|
-
status: "error",
|
|
286
|
-
error: err instanceof Error ? err.message : String(err),
|
|
287
|
-
});
|
|
298
|
+
for (const target of resolveInstructionTargets(options)) {
|
|
299
|
+
const body = target.importsFile !== undefined ? `@${target.importsFile}` : content;
|
|
300
|
+
entries.push(await writeTarget(target, body, managed, tag));
|
|
301
|
+
}
|
|
302
|
+
if (managed) {
|
|
303
|
+
for (const target of unrequestedPointerTargets(options)) {
|
|
304
|
+
const entry = await reconcilePointer(target, tag);
|
|
305
|
+
if (entry) entries.push(entry);
|
|
288
306
|
}
|
|
289
307
|
}
|
|
290
308
|
|
|
@@ -293,15 +311,84 @@ export async function installInstructions(
|
|
|
293
311
|
installed: entries.filter((e) => e.status === "created").length,
|
|
294
312
|
updated: entries.filter((e) => e.status === "updated").length,
|
|
295
313
|
skipped: entries.filter((e) => e.status === "skipped").length,
|
|
314
|
+
removed: entries.filter((e) => e.status === "removed").length,
|
|
296
315
|
errors: entries.filter((e) => e.status === "error").length,
|
|
297
316
|
};
|
|
298
317
|
}
|
|
299
318
|
|
|
319
|
+
async function writeTarget(
|
|
320
|
+
target: InstructionTarget,
|
|
321
|
+
body: string,
|
|
322
|
+
managed: boolean,
|
|
323
|
+
tag: string,
|
|
324
|
+
): Promise<InstructionInstallEntry> {
|
|
325
|
+
try {
|
|
326
|
+
const existing = await readFileOrNull(target.targetPath);
|
|
327
|
+
const next = managed ? upsertManagedBlock(existing, body, { tag }) : ensureTrailingNewline(body);
|
|
328
|
+
|
|
329
|
+
let status: InstructionStatus;
|
|
330
|
+
if (existing === null) {
|
|
331
|
+
status = "created";
|
|
332
|
+
} else if (existing === next) {
|
|
333
|
+
status = "skipped";
|
|
334
|
+
} else {
|
|
335
|
+
status = "updated";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (status !== "skipped") {
|
|
339
|
+
await fs.mkdir(path.dirname(target.targetPath), { recursive: true });
|
|
340
|
+
await fs.writeFile(target.targetPath, next, { mode: 0o644 });
|
|
341
|
+
}
|
|
342
|
+
return { ...target, status };
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return { ...target, status: "error", error: err instanceof Error ? err.message : String(err) };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Pointer files (a workspace CLAUDE.md) for the requested runtimes that the
|
|
350
|
+
* install doesn't write because the opt-in is off, but that still need
|
|
351
|
+
* reconciling if they exist.
|
|
352
|
+
*/
|
|
353
|
+
function unrequestedPointerTargets(options?: InstallInstructionsOptions): InstructionTarget[] {
|
|
354
|
+
if ((options?.location ?? "workspace") !== "workspace" || options?.includeNativeFiles) return [];
|
|
355
|
+
return resolveInstructionTargets({ ...options, includeNativeFiles: true }).filter(
|
|
356
|
+
(target) => target.importsFile !== undefined,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* An existing pointer file with the opt-in off: delete it when nothing but our
|
|
362
|
+
* managed region is in it, otherwise keep the user's content and make sure it
|
|
363
|
+
* imports the project file. Returns null when there is no file.
|
|
364
|
+
*/
|
|
365
|
+
async function reconcilePointer(
|
|
366
|
+
target: InstructionTarget,
|
|
367
|
+
tag: string,
|
|
368
|
+
): Promise<InstructionInstallEntry | null> {
|
|
369
|
+
try {
|
|
370
|
+
const existing = await readFileOrNull(target.targetPath);
|
|
371
|
+
if (existing === null) return null;
|
|
372
|
+
if (stripManagedBlock(existing, { tag }) === null) {
|
|
373
|
+
await fs.rm(target.targetPath, { force: true });
|
|
374
|
+
return { ...target, status: "removed" };
|
|
375
|
+
}
|
|
376
|
+
const next = upsertManagedBlock(existing, `@${target.importsFile}`, { tag });
|
|
377
|
+
if (next === existing) return { ...target, status: "skipped" };
|
|
378
|
+
await fs.writeFile(target.targetPath, next);
|
|
379
|
+
return { ...target, status: "updated" };
|
|
380
|
+
} catch (err) {
|
|
381
|
+
return { ...target, status: "error", error: err instanceof Error ? err.message : String(err) };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
300
385
|
/**
|
|
301
386
|
* Remove the managed region installed by {@link installInstructions}, preserving
|
|
302
387
|
* any user-authored content outside the markers. If the file contains nothing
|
|
303
388
|
* but the managed block, it is deleted. User-owned files (no managed region) are
|
|
304
|
-
* left untouched and reported as "skipped".
|
|
389
|
+
* left untouched and reported as "skipped". Native files (CLAUDE.md, GEMINI.md)
|
|
390
|
+
* are always checked, whether or not they were installed with
|
|
391
|
+
* `includeNativeFiles`.
|
|
305
392
|
*/
|
|
306
393
|
export async function removeInstructions(
|
|
307
394
|
options?: RemoveInstructionsOptions,
|
|
@@ -311,6 +398,7 @@ export async function removeInstructions(
|
|
|
311
398
|
runtimes: options?.runtimes,
|
|
312
399
|
location: options?.location,
|
|
313
400
|
cwd: options?.cwd,
|
|
401
|
+
includeNativeFiles: true,
|
|
314
402
|
homeDir: options?.homeDir,
|
|
315
403
|
});
|
|
316
404
|
const entries: InstructionRemoveEntry[] = [];
|