@first-tree-ai/context-tree 0.1.7-alpha.202609010710 → 0.1.7

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 (37) hide show
  1. package/README.md +35 -38
  2. package/dist/cli/index.mjs +923 -647
  3. package/package.json +7 -28
  4. package/scripts/postinstall.mjs +53 -0
  5. package/skills/context-tree-connect/SKILL.md +9 -7
  6. package/skills/context-tree-create/SKILL.md +10 -10
  7. package/skills/context-tree-publish/SKILL.md +4 -8
  8. package/skills/context-tree-read/SKILL.md +32 -12
  9. package/skills/context-tree-setup/SKILL.md +7 -12
  10. package/skills/context-tree-write/SKILL.md +196 -17
  11. package/.agents/plugins/marketplace.json +0 -19
  12. package/.claude-plugin/marketplace.json +0 -21
  13. package/.claude-plugin/plugin.json +0 -16
  14. package/.codex-plugin/plugin.json +0 -36
  15. package/dist/cli/index.d.mts +0 -1
  16. package/dist/index.d.mts +0 -77
  17. package/dist/index.mjs +0 -1449
  18. package/dist/schemas-C_7izpsa.d.mts +0 -390
  19. package/dist/schemas-DKHE1sWt.mjs +0 -289
  20. package/dist/schemas.d.mts +0 -2
  21. package/dist/schemas.mjs +0 -2
  22. package/docs/specification.md +0 -160
  23. package/examples/basic/NODE.md +0 -15
  24. package/examples/basic/members/NODE.md +0 -8
  25. package/examples/basic/members/example-agent/NODE.md +0 -7
  26. package/examples/basic/members/example-agent/memory.md +0 -9
  27. package/examples/basic/systems/NODE.md +0 -10
  28. package/examples/basic/systems/runtime.md +0 -15
  29. package/hooks/hooks.json +0 -26
  30. package/hooks/session-start.mjs +0 -55
  31. package/policy/context-tree-policy.md +0 -156
  32. package/skills/context-tree-connect/scripts/context-tree.mjs +0 -41
  33. package/skills/context-tree-create/scripts/context-tree.mjs +0 -41
  34. package/skills/context-tree-publish/scripts/context-tree.mjs +0 -41
  35. package/skills/context-tree-read/scripts/context-tree.mjs +0 -41
  36. package/skills/context-tree-setup/scripts/context-tree.mjs +0 -41
  37. package/skills/context-tree-write/scripts/context-tree.mjs +0 -41
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import path, { basename, dirname, isAbsolute, join, parse, posix, relative, resolve, sep } from "node:path";
4
4
  import { EventEmitter } from "node:events";
5
5
  import childProcess, { spawnSync } from "node:child_process";
6
- import fs, { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
6
+ import fs, { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
7
7
  import process$1 from "node:process";
8
8
  import { stripVTControlCharacters } from "node:util";
9
9
  import { homedir, tmpdir } from "node:os";
@@ -7240,6 +7240,448 @@ function refine(fn, _params = {}) {
7240
7240
  function superRefine(fn, params) {
7241
7241
  return /* @__PURE__ */ _superRefine(fn, params);
7242
7242
  }
7243
+ const CONTEXT_TREE_ROOT_NODE_MAX_BYTES = 16 * 1024;
7244
+ const VALIDATION_CODES = {
7245
+ rootMissing: "TREE_ROOT_MISSING",
7246
+ rootNodeInvalid: "TREE_ROOT_NODE_INVALID",
7247
+ rootOnlyFields: "TREE_ROOT_ONLY_FIELDS",
7248
+ directoryNodeMissing: "TREE_DIRECTORY_NODE_MISSING",
7249
+ frontmatterMissing: "TREE_FRONTMATTER_MISSING",
7250
+ frontmatterParse: "TREE_FRONTMATTER_PARSE",
7251
+ titleMissing: "TREE_TITLE_MISSING",
7252
+ titleInvalid: "TREE_TITLE_INVALID",
7253
+ descriptionInvalid: "TREE_DESCRIPTION_INVALID",
7254
+ softLinksInvalid: "TREE_SOFT_LINKS_INVALID",
7255
+ softLinkBroken: "TREE_SOFT_LINK_BROKEN",
7256
+ softLinkPathEscape: "TREE_SOFT_LINK_PATH_ESCAPE",
7257
+ markdownPathEscape: "TREE_MARKDOWN_LINK_PATH_ESCAPE",
7258
+ markdownFileSymlinkBroken: "TREE_MARKDOWN_FILE_SYMLINK_BROKEN",
7259
+ markdownFileSymlinkUnsupported: "TREE_MARKDOWN_FILE_SYMLINK_UNSUPPORTED",
7260
+ markdownFilePathEscape: "TREE_MARKDOWN_FILE_PATH_ESCAPE",
7261
+ markdownFileContentClassMismatch: "TREE_MARKDOWN_FILE_CONTENT_CLASS_MISMATCH",
7262
+ directorySymlinkUnsupported: "TREE_DIRECTORY_SYMLINK_UNSUPPORTED",
7263
+ directorySymlinkPathEscape: "TREE_DIRECTORY_SYMLINK_PATH_ESCAPE"
7264
+ };
7265
+ const CLI_ERROR_CODES = {
7266
+ corruptConnection: "CORRUPT_CONNECTION",
7267
+ dirtyTree: "DIRTY_TREE",
7268
+ failed: "CONTEXT_TREE_FAILED",
7269
+ githubAuth: "GITHUB_AUTH",
7270
+ invalidTree: "INVALID_TREE",
7271
+ noConnection: "NO_CONNECTION",
7272
+ publishIncomplete: "PUBLISH_INCOMPLETE",
7273
+ repositoryExists: "REPOSITORY_EXISTS",
7274
+ staleConnection: "STALE_CONNECTION",
7275
+ writeOutdated: "WRITE_OUTDATED"
7276
+ };
7277
+ function hasUnsafeCharacter(value) {
7278
+ return [...value].some((character) => {
7279
+ const code = character.codePointAt(0);
7280
+ return code !== void 0 && (code <= 31 || code === 127 || code === 8232 || code === 8233);
7281
+ });
7282
+ }
7283
+ const absoluteSingleLinePathSchema = string$2().superRefine((value, context) => {
7284
+ if (!isAbsolute(value) || value.trim() !== value || hasUnsafeCharacter(value)) context.addIssue({
7285
+ code: "custom",
7286
+ message: "Paths must be absolute single-line values."
7287
+ });
7288
+ });
7289
+ const credentialFreeRepositoryUrlSchema = string$2().superRefine((value, context) => {
7290
+ if (value.trim() !== value || hasUnsafeCharacter(value) || value.includes("\\")) {
7291
+ context.addIssue({
7292
+ code: "custom",
7293
+ message: "Repository URLs must be canonical single-line values."
7294
+ });
7295
+ return;
7296
+ }
7297
+ if (value.includes("?") || value.includes("#")) {
7298
+ context.addIssue({
7299
+ code: "custom",
7300
+ message: "Repository URLs must not contain a query or fragment."
7301
+ });
7302
+ return;
7303
+ }
7304
+ if (/^(?:[a-z\d._-]+@)?[a-z\d.-]+:[^\s/][^\s]*$/iu.test(value)) return;
7305
+ try {
7306
+ const parsed = new URL(value);
7307
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "ssh:" || !parsed.hostname || parsed.pathname.split("/").every((part) => part.length === 0) || parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) throw new Error("invalid transport");
7308
+ } catch {
7309
+ context.addIssue({
7310
+ code: "custom",
7311
+ message: "Repository URLs must use credential-free HTTP(S), ssh://, or scp-like SSH syntax."
7312
+ });
7313
+ }
7314
+ });
7315
+ const treeNameSchema = string$2().superRefine((value, context) => {
7316
+ if (!/^[A-Za-z\d][A-Za-z\d._-]{0,99}$/u.test(value) || /\.git$/iu.test(value)) context.addIssue({
7317
+ code: "custom",
7318
+ message: "Tree name must be a safe single path segment."
7319
+ });
7320
+ });
7321
+ const githubRepositoryIdentitySchema = string$2().superRefine((value, context) => {
7322
+ const parts = value.split("/");
7323
+ const [owner, name] = parts;
7324
+ if (parts.length !== 2 || owner === void 0 || name === void 0 || !/^[A-Za-z\d](?:[A-Za-z\d-]{0,37}[A-Za-z\d])?$/u.test(owner) || !treeNameSchema.safeParse(name).success) context.addIssue({
7325
+ code: "custom",
7326
+ message: "Repository must be an explicit GitHub OWNER/REPO identity."
7327
+ });
7328
+ });
7329
+ const contextTreeRootNodeSchema = object({
7330
+ frontmatter: object({
7331
+ schemaVersion: literal(1),
7332
+ title: string$2().trim().min(1),
7333
+ description: string$2().trim().min(1).optional(),
7334
+ soft_links: array(string$2().trim().min(1)).min(1).optional()
7335
+ }).loose(),
7336
+ body: string$2().trim().min(1, "Root NODE.md must contain repository-wide context.")
7337
+ });
7338
+ const contextContentClassSchema = _enum([
7339
+ "normal",
7340
+ "member",
7341
+ "repo-infra"
7342
+ ]);
7343
+ const treeValidationFindingSchema = object({
7344
+ code: _enum(VALIDATION_CODES),
7345
+ message: string$2(),
7346
+ path: string$2(),
7347
+ target: string$2().optional()
7348
+ }).strict();
7349
+ const contextContentClassCountsSchema = object({
7350
+ normal: number().int().nonnegative(),
7351
+ member: number().int().nonnegative(),
7352
+ "repo-infra": number().int().nonnegative()
7353
+ }).strict();
7354
+ const SKILL_HOSTS = ["claude", "codex"];
7355
+ const skillHostSchema = _enum(SKILL_HOSTS);
7356
+ const skillInstallationSchema = object({
7357
+ host: skillHostSchema,
7358
+ path: absoluteSingleLinePathSchema,
7359
+ skills: array(string$2().trim().min(1))
7360
+ }).strict();
7361
+ const skillInstallSkipSchema = object({
7362
+ host: skillHostSchema,
7363
+ reason: string$2().trim().min(1)
7364
+ }).strict();
7365
+ object({
7366
+ installed: array(skillInstallationSchema),
7367
+ schemaVersion: literal(1),
7368
+ skipped: array(skillInstallSkipSchema),
7369
+ version: string$2().trim().min(1)
7370
+ }).strict();
7371
+ const contextTreeReadCommonFields = {
7372
+ contentClass: contextContentClassSchema,
7373
+ kind: _enum(["directory", "file"]),
7374
+ path: string$2()
7375
+ };
7376
+ const contextTreeReadNodeSchema = object({
7377
+ body: string$2(),
7378
+ frontmatter: record(string$2(), unknown()),
7379
+ ...contextTreeReadCommonFields
7380
+ }).strict();
7381
+ object({
7382
+ children: array(object({
7383
+ description: string$2().optional(),
7384
+ title: string$2(),
7385
+ ...contextTreeReadCommonFields
7386
+ }).strict()),
7387
+ node: contextTreeReadNodeSchema,
7388
+ root: string$2(),
7389
+ schemaVersion: literal(1),
7390
+ target: string$2()
7391
+ }).strict();
7392
+ object({
7393
+ findings: array(treeValidationFindingSchema),
7394
+ ok: boolean(),
7395
+ root: string$2(),
7396
+ scannedByContentClass: contextContentClassCountsSchema,
7397
+ schemaVersion: literal(1)
7398
+ }).strict();
7399
+ const contextTreeStateSchema = discriminatedUnion("kind", [object({
7400
+ kind: literal("local"),
7401
+ path: absoluteSingleLinePathSchema
7402
+ }).strict(), object({
7403
+ kind: literal("github"),
7404
+ path: absoluteSingleLinePathSchema,
7405
+ repository: githubRepositoryIdentitySchema
7406
+ }).strict()]);
7407
+ const contextTreeConnectionSchema = object({
7408
+ projectPath: absoluteSingleLinePathSchema,
7409
+ tree: contextTreeStateSchema
7410
+ }).strict();
7411
+ object({
7412
+ schemaVersion: literal(1),
7413
+ tree: contextTreeStateSchema
7414
+ }).strict();
7415
+ /** Whether the project's AGENTS.md pointer was created, rewritten, or left alone. */
7416
+ const projectPointerOutcomeSchema = _enum([
7417
+ "written",
7418
+ "updated",
7419
+ "skipped"
7420
+ ]);
7421
+ object({
7422
+ branch: string$2().trim().min(1),
7423
+ commitSha: string$2(),
7424
+ created: boolean(),
7425
+ pointer: projectPointerOutcomeSchema,
7426
+ schemaVersion: literal(1),
7427
+ title: string$2().trim().min(1),
7428
+ treePath: absoluteSingleLinePathSchema
7429
+ }).strict();
7430
+ object({
7431
+ pointer: projectPointerOutcomeSchema,
7432
+ schemaVersion: literal(1),
7433
+ tree: contextTreeStateSchema
7434
+ }).strict();
7435
+ const managedTreeListingEntrySchema = object({
7436
+ name: treeNameSchema,
7437
+ tree: contextTreeStateSchema
7438
+ }).strict();
7439
+ object({
7440
+ schemaVersion: literal(1),
7441
+ trees: array(managedTreeListingEntrySchema)
7442
+ }).strict();
7443
+ object({
7444
+ branch: string$2().trim().min(1),
7445
+ schemaVersion: literal(1),
7446
+ sha: string$2(),
7447
+ tree: contextTreeStateSchema
7448
+ }).strict();
7449
+ object({
7450
+ schemaVersion: literal(1),
7451
+ worktreePath: absoluteSingleLinePathSchema
7452
+ }).strict();
7453
+ object({
7454
+ branch: string$2().trim().min(1),
7455
+ schemaVersion: literal(1),
7456
+ sha: string$2()
7457
+ }).strict();
7458
+ object({
7459
+ branch: string$2().trim().min(1),
7460
+ repository: githubRepositoryIdentitySchema,
7461
+ schemaVersion: literal(1),
7462
+ sha: string$2(),
7463
+ url: credentialFreeRepositoryUrlSchema
7464
+ }).strict();
7465
+ object({
7466
+ error: object({
7467
+ code: _enum(CLI_ERROR_CODES),
7468
+ message: string$2()
7469
+ }).strict(),
7470
+ ok: literal(false),
7471
+ schemaVersion: literal(1)
7472
+ }).strict();
7473
+ //#endregion
7474
+ //#region src/core/internal/errors.ts
7475
+ /**
7476
+ * A failure the CLI reports with a specific machine-readable code. Anything
7477
+ * thrown as a plain Error is reported as CONTEXT_TREE_FAILED instead.
7478
+ */
7479
+ var ContextTreeError = class extends Error {
7480
+ code;
7481
+ constructor(code, message) {
7482
+ super(message);
7483
+ this.name = "ContextTreeError";
7484
+ this.code = code;
7485
+ }
7486
+ };
7487
+ //#endregion
7488
+ //#region src/core/internal/git.ts
7489
+ function defaultRunner(command, args) {
7490
+ const result = spawnSync(command, args, {
7491
+ encoding: "utf8",
7492
+ stdio: [
7493
+ "ignore",
7494
+ "pipe",
7495
+ "pipe"
7496
+ ]
7497
+ });
7498
+ return {
7499
+ status: result.status,
7500
+ stderr: typeof result.stderr === "string" ? result.stderr : "",
7501
+ stdout: typeof result.stdout === "string" ? result.stdout : ""
7502
+ };
7503
+ }
7504
+ /** A failed Git or `gh` operation. Messages never include the argv. */
7505
+ var CommandError = class extends Error {
7506
+ command;
7507
+ status;
7508
+ stderr;
7509
+ constructor(command, status, stderr, message) {
7510
+ const detail = sanitizeCommandOutput(stderr).trim();
7511
+ super(detail.length > 0 ? `${message}: ${detail}` : message);
7512
+ this.name = "CommandError";
7513
+ this.command = command;
7514
+ this.status = status;
7515
+ this.stderr = detail;
7516
+ }
7517
+ };
7518
+ /** Remove credentials and common access-token shapes before surfacing subprocess output. */
7519
+ function sanitizeCommandOutput(value) {
7520
+ return value.replace(/((?:https?|ssh):\/\/)[^\s/@]+@/giu, "$1<redacted>@").replace(/\b(?:gh[opsu]_[A-Za-z\d_]{20,}|github_pat_[A-Za-z\d_]{20,})\b/gu, "<redacted>").replace(/(authorization\s*:\s*(?:bearer|token)\s+)[^\s]+/giu, "$1<redacted>");
7521
+ }
7522
+ function trimOutput(value) {
7523
+ return value.trim();
7524
+ }
7525
+ function execute(runner, command, args, message) {
7526
+ const result = runner(command, args);
7527
+ if (result.status !== 0) throw new CommandError(command, result.status, result.stderr, message);
7528
+ return trimOutput(result.stdout);
7529
+ }
7530
+ /** Run a Git command that is not scoped by `-C`, such as `git init <path>`. */
7531
+ function gitCommand(args, options = {}) {
7532
+ return execute(options.runner ?? defaultRunner, "git", args, options.message ?? "A Git operation failed.");
7533
+ }
7534
+ /** Run `git -C <root> <args>` and return trimmed stdout, throwing on failure. */
7535
+ function git(root, args, options = {}) {
7536
+ return execute(options.runner ?? defaultRunner, "git", [
7537
+ "-C",
7538
+ root,
7539
+ ...args
7540
+ ], options.message ?? "A Git operation failed.");
7541
+ }
7542
+ /** Run `git -C <root> <args>` and return trimmed stdout, or undefined on failure. */
7543
+ function optionalGit(root, args, runner = defaultRunner) {
7544
+ const result = runner("git", [
7545
+ "-C",
7546
+ root,
7547
+ ...args
7548
+ ]);
7549
+ if (result.status !== 0) return void 0;
7550
+ return trimOutput(result.stdout);
7551
+ }
7552
+ /** Run `gh <args>` and return trimmed stdout, throwing on failure. */
7553
+ function gh(args, options = {}) {
7554
+ return execute(options.runner ?? defaultRunner, "gh", args, options.message ?? "A GitHub CLI operation failed.");
7555
+ }
7556
+ //#endregion
7557
+ //#region src/core/internal/github-repository.ts
7558
+ const ORIGIN_MESSAGE = "Context Tree origin must identify a credential-free GitHub OWNER/REPO repository.";
7559
+ function canonicalGitHubRepositoryUrl(repository) {
7560
+ githubRepositoryIdentitySchema.parse(repository);
7561
+ return `https://github.com/${repository}.git`;
7562
+ }
7563
+ /** Derive OWNER/REPO from a github.com origin, rejecting anything else. */
7564
+ function gitHubRepositoryFromOriginUrl(origin) {
7565
+ if (!credentialFreeRepositoryUrlSchema.safeParse(origin).success) throw new Error(ORIGIN_MESSAGE);
7566
+ let owner;
7567
+ let name;
7568
+ const scp = /^(?:git@)?github\.com:([^/]+)\/(.+)$/iu.exec(origin);
7569
+ if (scp !== null) [, owner, name] = scp;
7570
+ else {
7571
+ const url = URL.parse(origin);
7572
+ if (url === null || url.hostname.toLowerCase() !== "github.com") throw new Error(ORIGIN_MESSAGE);
7573
+ [owner, name] = url.pathname.replace(/^\/+|\/+$/gu, "").split("/");
7574
+ }
7575
+ const repository = `${owner ?? ""}/${(name ?? "").replace(/\.git$/iu, "")}`;
7576
+ if (!githubRepositoryIdentitySchema.safeParse(repository).success) throw new Error(ORIGIN_MESSAGE);
7577
+ return repository;
7578
+ }
7579
+ //#endregion
7580
+ //#region src/core/path.ts
7581
+ function isPathInside(root, target) {
7582
+ const path = relative(root, target);
7583
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
7584
+ }
7585
+ function resolveTreeRoot(path) {
7586
+ const absolute = resolve(path);
7587
+ const entry = lstatSync(absolute);
7588
+ if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
7589
+ return realpathSync(absolute);
7590
+ }
7591
+ /** Resolve a directory while rejecting symlinks in user-controlled path components. */
7592
+ function realDirectoryWithoutSymlinks(path, label) {
7593
+ const absolute = resolve(path);
7594
+ const parsed = parse(absolute);
7595
+ const parts = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
7596
+ let current = parsed.root;
7597
+ for (const [index, part] of parts.entries()) {
7598
+ current = resolve(current, part);
7599
+ if (lstatSync(current).isSymbolicLink()) if (index === 0) current = realpathSync(current);
7600
+ else throw new Error(`${label} must contain no symlink component.`);
7601
+ }
7602
+ if (!lstatSync(current).isDirectory()) throw new Error(`${label} must be a directory.`);
7603
+ return realpathSync(current);
7604
+ }
7605
+ function toPosixPath(path) {
7606
+ return path.replace(/\\/gu, "/");
7607
+ }
7608
+ //#endregion
7609
+ //#region src/core/internal/project.ts
7610
+ /**
7611
+ * Projects are identified solely by their canonical local root. A Git
7612
+ * repository without an origin, a non-Git directory, a Git worktree, and a
7613
+ * separate clone are all independent checkouts with their own canonical root.
7614
+ */
7615
+ function canonicalProjectRoot(path, runner) {
7616
+ const directory = realDirectoryWithoutSymlinks(path, "Project path");
7617
+ const toplevel = optionalGit(directory, ["rev-parse", "--show-toplevel"], runner);
7618
+ if (toplevel === void 0 || toplevel.length === 0) return directory;
7619
+ return realpathSync(toplevel);
7620
+ }
7621
+ //#endregion
7622
+ //#region src/core/internal/project-pointer.ts
7623
+ const BEGIN = "<!-- context-tree:begin -->";
7624
+ const END = "<!-- context-tree:end -->";
7625
+ function pointerBlock(treePath) {
7626
+ return [
7627
+ BEGIN,
7628
+ "## Context Tree",
7629
+ "",
7630
+ `This project is connected to a Context Tree at \`${treePath}\`.`,
7631
+ "",
7632
+ "Read the decisions and constraints that bear on a task before planning or",
7633
+ "changing code, and record durable decisions there. Use the `context-tree-read`",
7634
+ "and `context-tree-write` skills rather than editing the tree by hand.",
7635
+ END
7636
+ ].join("\n");
7637
+ }
7638
+ /** A real regular file, a real absent path, or a refusal. */
7639
+ function regularFileContent(path) {
7640
+ const entry = lstatSync(path, { throwIfNoEntry: false });
7641
+ if (entry === void 0) return void 0;
7642
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Refusing to write the Context Tree pointer through a symlink or non-file: ${path}`);
7643
+ return readFileSync(path, "utf8");
7644
+ }
7645
+ /**
7646
+ * Record the connected tree in the project's own `AGENTS.md`, so any agent that reads
7647
+ * instruction files knows the tree exists without a host-specific session hook.
7648
+ *
7649
+ * Idempotent: a delimited block is rewritten in place, never appended twice, so
7650
+ * switching the connected tree updates the existing pointer. All other content is
7651
+ * preserved, and an existing regular `CLAUDE.md` is left alone.
7652
+ */
7653
+ function writeProjectPointer(projectPath, treePath) {
7654
+ const agentsPath = join(projectPath, "AGENTS.md");
7655
+ const block = pointerBlock(treePath);
7656
+ const existing = regularFileContent(agentsPath);
7657
+ if (existing === void 0) {
7658
+ writeFileSync(agentsPath, `# AGENTS.md\n\n${block}\n`, {
7659
+ encoding: "utf8",
7660
+ mode: 420
7661
+ });
7662
+ linkClaudeMarkdown(projectPath);
7663
+ return "written";
7664
+ }
7665
+ const start = existing.indexOf(BEGIN);
7666
+ const end = existing.indexOf(END);
7667
+ if (start !== -1 && end > start) {
7668
+ const replaced = `${existing.slice(0, start)}${block}${existing.slice(end + 25)}`;
7669
+ if (replaced === existing) return "skipped";
7670
+ writeFileSync(agentsPath, replaced, { encoding: "utf8" });
7671
+ return "updated";
7672
+ }
7673
+ writeFileSync(agentsPath, `${existing}${existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n"}${block}\n`, { encoding: "utf8" });
7674
+ linkClaudeMarkdown(projectPath);
7675
+ return "written";
7676
+ }
7677
+ /** Point CLAUDE.md at AGENTS.md only when the project has no CLAUDE.md of its own. */
7678
+ function linkClaudeMarkdown(projectPath) {
7679
+ const claudePath = join(projectPath, "CLAUDE.md");
7680
+ if (lstatSync(claudePath, { throwIfNoEntry: false }) !== void 0) return;
7681
+ try {
7682
+ symlinkSync("AGENTS.md", claudePath, "file");
7683
+ } catch {}
7684
+ }
7243
7685
  //#endregion
7244
7686
  //#region node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
7245
7687
  var require_identity = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -13728,557 +14170,220 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => {
13728
14170
  var identity = require_identity();
13729
14171
  var lineCounter = require_line_counter();
13730
14172
  var parser = require_parser();
13731
- function parseOptions(options) {
13732
- const prettyErrors = options.prettyErrors !== false;
13733
- return {
13734
- lineCounter: options.lineCounter || prettyErrors && new lineCounter.LineCounter() || null,
13735
- prettyErrors
13736
- };
13737
- }
13738
- /**
13739
- * Parse the input as a stream of YAML documents.
13740
- *
13741
- * Documents should be separated from each other by `...` or `---` marker lines.
13742
- *
13743
- * @returns If an empty `docs` array is returned, it will be of type
13744
- * EmptyStream and contain additional stream information. In
13745
- * TypeScript, you should use `'empty' in docs` as a type guard for it.
13746
- */
13747
- function parseAllDocuments(source, options = {}) {
13748
- const { lineCounter, prettyErrors } = parseOptions(options);
13749
- const parser$1 = new parser.Parser(lineCounter?.addNewLine);
13750
- const composer$1 = new composer.Composer(options);
13751
- const docs = Array.from(composer$1.compose(parser$1.parse(source)));
13752
- if (prettyErrors && lineCounter) for (const doc of docs) {
13753
- doc.errors.forEach(errors.prettifyError(source, lineCounter));
13754
- doc.warnings.forEach(errors.prettifyError(source, lineCounter));
13755
- }
13756
- if (docs.length > 0) return docs;
13757
- return Object.assign([], { empty: true }, composer$1.streamInfo());
13758
- }
13759
- /** Parse an input string into a single YAML.Document */
13760
- function parseDocument(source, options = {}) {
13761
- const { lineCounter, prettyErrors } = parseOptions(options);
13762
- const parser$1 = new parser.Parser(lineCounter?.addNewLine);
13763
- const composer$1 = new composer.Composer(options);
13764
- let doc = null;
13765
- for (const _doc of composer$1.compose(parser$1.parse(source), true, source.length)) if (!doc) doc = _doc;
13766
- else if (doc.options.logLevel !== "silent") {
13767
- doc.errors.push(new errors.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
13768
- break;
13769
- }
13770
- if (prettyErrors && lineCounter) {
13771
- doc.errors.forEach(errors.prettifyError(source, lineCounter));
13772
- doc.warnings.forEach(errors.prettifyError(source, lineCounter));
13773
- }
13774
- return doc;
13775
- }
13776
- function parse(src, reviver, options) {
13777
- let _reviver = void 0;
13778
- if (typeof reviver === "function") _reviver = reviver;
13779
- else if (options === void 0 && reviver && typeof reviver === "object") options = reviver;
13780
- const doc = parseDocument(src, options);
13781
- if (!doc) return null;
13782
- doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
13783
- if (doc.errors.length > 0) if (doc.options.logLevel !== "silent") throw doc.errors[0];
13784
- else doc.errors = [];
13785
- return doc.toJS(Object.assign({ reviver: _reviver }, options));
13786
- }
13787
- function stringify(value, replacer, options) {
13788
- let _replacer = null;
13789
- if (typeof replacer === "function" || Array.isArray(replacer)) _replacer = replacer;
13790
- else if (options === void 0 && replacer) options = replacer;
13791
- if (typeof options === "string") options = options.length;
13792
- if (typeof options === "number") {
13793
- const indent = Math.round(options);
13794
- options = indent < 1 ? void 0 : indent > 8 ? { indent: 8 } : { indent };
13795
- }
13796
- if (value === void 0) {
13797
- const { keepUndefined } = options ?? replacer ?? {};
13798
- if (!keepUndefined) return void 0;
13799
- }
13800
- if (identity.isDocument(value) && !_replacer) return value.toString(options);
13801
- return new Document.Document(value, _replacer, options).toString(options);
13802
- }
13803
- exports.parse = parse;
13804
- exports.parseAllDocuments = parseAllDocuments;
13805
- exports.parseDocument = parseDocument;
13806
- exports.stringify = stringify;
13807
- }));
13808
- //#endregion
13809
- //#region src/internal/value.ts
13810
- var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
13811
- var composer = require_composer();
13812
- var Document = require_Document();
13813
- var Schema = require_Schema();
13814
- var errors = require_errors();
13815
- var Alias = require_Alias();
13816
- var identity = require_identity();
13817
- var Pair = require_Pair();
13818
- var Scalar = require_Scalar();
13819
- var YAMLMap = require_YAMLMap();
13820
- var YAMLSeq = require_YAMLSeq();
13821
- require_cst();
13822
- var lexer = require_lexer();
13823
- var lineCounter = require_line_counter();
13824
- var parser = require_parser();
13825
- var publicApi = require_public_api();
13826
- var visit = require_visit();
13827
- exports.Composer = composer.Composer;
13828
- exports.Document = Document.Document;
13829
- exports.Schema = Schema.Schema;
13830
- exports.YAMLError = errors.YAMLError;
13831
- exports.YAMLParseError = errors.YAMLParseError;
13832
- exports.YAMLWarning = errors.YAMLWarning;
13833
- exports.Alias = Alias.Alias;
13834
- exports.isAlias = identity.isAlias;
13835
- exports.isCollection = identity.isCollection;
13836
- exports.isDocument = identity.isDocument;
13837
- exports.isMap = identity.isMap;
13838
- exports.isNode = identity.isNode;
13839
- exports.isPair = identity.isPair;
13840
- exports.isScalar = identity.isScalar;
13841
- exports.isSeq = identity.isSeq;
13842
- exports.Pair = Pair.Pair;
13843
- exports.Scalar = Scalar.Scalar;
13844
- exports.YAMLMap = YAMLMap.YAMLMap;
13845
- exports.YAMLSeq = YAMLSeq.YAMLSeq;
13846
- exports.Lexer = lexer.Lexer;
13847
- exports.LineCounter = lineCounter.LineCounter;
13848
- exports.Parser = parser.Parser;
13849
- exports.parse = publicApi.parse;
13850
- exports.parseAllDocuments = publicApi.parseAllDocuments;
13851
- exports.parseDocument = publicApi.parseDocument;
13852
- exports.stringify = publicApi.stringify;
13853
- exports.visit = visit.visit;
13854
- exports.visitAsync = visit.visitAsync;
13855
- })))();
13856
- function isRecord(value) {
13857
- return typeof value === "object" && value !== null && !Array.isArray(value);
13858
- }
13859
- //#endregion
13860
- //#region src/internal/frontmatter.ts
13861
- function parseYamlMapping(source) {
13862
- const value = (0, import_dist.parse)(source);
13863
- if (!isRecord(value)) throw new Error("frontmatter must be a YAML mapping");
13864
- return value;
13865
- }
13866
- function readLine(source, start) {
13867
- const newline = source.indexOf("\n", start);
13868
- const end = newline === -1 ? source.length : newline + 1;
13869
- const contentEnd = newline === -1 ? source.length : source.charCodeAt(newline - 1) === 13 ? newline - 1 : newline;
13870
- return {
13871
- end,
13872
- start,
13873
- value: source.slice(start, contentEnd)
13874
- };
13875
- }
13876
- function parseMarkdownFrontmatter(source) {
13877
- const opening = readLine(source, 0);
13878
- if (opening.value !== "---") return {
13879
- body: source,
13880
- data: null,
13881
- frontmatter: "missing"
13882
- };
13883
- let closing;
13884
- let lineStart = opening.end;
13885
- while (lineStart < source.length) {
13886
- const line = readLine(source, lineStart);
13887
- if (line.value === "---") {
13888
- closing = line;
13889
- break;
13890
- }
13891
- lineStart = line.end;
13892
- }
13893
- if (closing === void 0) return {
13894
- body: "",
13895
- data: null,
13896
- error: "frontmatter closing delimiter is missing",
13897
- frontmatter: "invalid"
13898
- };
13899
- const body = source.slice(closing.end);
13900
- try {
13901
- return {
13902
- body,
13903
- data: parseYamlMapping(source.slice(opening.end, closing.start)),
13904
- frontmatter: "valid"
13905
- };
13906
- } catch (error) {
14173
+ function parseOptions(options) {
14174
+ const prettyErrors = options.prettyErrors !== false;
13907
14175
  return {
13908
- body,
13909
- data: null,
13910
- error: error instanceof Error ? error.message : String(error),
13911
- frontmatter: "invalid"
14176
+ lineCounter: options.lineCounter || prettyErrors && new lineCounter.LineCounter() || null,
14177
+ prettyErrors
13912
14178
  };
13913
14179
  }
13914
- }
13915
- const CONTEXT_TREE_ROOT_NODE_MAX_BYTES = 16 * 1024;
13916
- const VALIDATION_CODES = {
13917
- rootMissing: "TREE_ROOT_MISSING",
13918
- rootNodeInvalid: "TREE_ROOT_NODE_INVALID",
13919
- rootOnlyFields: "TREE_ROOT_ONLY_FIELDS",
13920
- directoryNodeMissing: "TREE_DIRECTORY_NODE_MISSING",
13921
- frontmatterMissing: "TREE_FRONTMATTER_MISSING",
13922
- frontmatterParse: "TREE_FRONTMATTER_PARSE",
13923
- titleMissing: "TREE_TITLE_MISSING",
13924
- titleInvalid: "TREE_TITLE_INVALID",
13925
- descriptionInvalid: "TREE_DESCRIPTION_INVALID",
13926
- softLinksInvalid: "TREE_SOFT_LINKS_INVALID",
13927
- softLinkBroken: "TREE_SOFT_LINK_BROKEN",
13928
- softLinkPathEscape: "TREE_SOFT_LINK_PATH_ESCAPE",
13929
- markdownPathEscape: "TREE_MARKDOWN_LINK_PATH_ESCAPE",
13930
- markdownFileSymlinkBroken: "TREE_MARKDOWN_FILE_SYMLINK_BROKEN",
13931
- markdownFileSymlinkUnsupported: "TREE_MARKDOWN_FILE_SYMLINK_UNSUPPORTED",
13932
- markdownFilePathEscape: "TREE_MARKDOWN_FILE_PATH_ESCAPE",
13933
- markdownFileContentClassMismatch: "TREE_MARKDOWN_FILE_CONTENT_CLASS_MISMATCH",
13934
- directorySymlinkUnsupported: "TREE_DIRECTORY_SYMLINK_UNSUPPORTED",
13935
- directorySymlinkPathEscape: "TREE_DIRECTORY_SYMLINK_PATH_ESCAPE"
13936
- };
13937
- const CLI_ERROR_CODES = {
13938
- corruptConnection: "CORRUPT_CONNECTION",
13939
- dirtyTree: "DIRTY_TREE",
13940
- failed: "CONTEXT_TREE_FAILED",
13941
- githubAuth: "GITHUB_AUTH",
13942
- invalidTree: "INVALID_TREE",
13943
- noConnection: "NO_CONNECTION",
13944
- publishIncomplete: "PUBLISH_INCOMPLETE",
13945
- repositoryExists: "REPOSITORY_EXISTS",
13946
- staleConnection: "STALE_CONNECTION",
13947
- writeOutdated: "WRITE_OUTDATED"
13948
- };
13949
- function hasUnsafeCharacter(value) {
13950
- return [...value].some((character) => {
13951
- const code = character.codePointAt(0);
13952
- return code !== void 0 && (code <= 31 || code === 127 || code === 8232 || code === 8233);
13953
- });
13954
- }
13955
- const absoluteSingleLinePathSchema = string$2().superRefine((value, context) => {
13956
- if (!isAbsolute(value) || value.trim() !== value || hasUnsafeCharacter(value)) context.addIssue({
13957
- code: "custom",
13958
- message: "Paths must be absolute single-line values."
13959
- });
13960
- });
13961
- const credentialFreeRepositoryUrlSchema = string$2().superRefine((value, context) => {
13962
- if (value.trim() !== value || hasUnsafeCharacter(value) || value.includes("\\")) {
13963
- context.addIssue({
13964
- code: "custom",
13965
- message: "Repository URLs must be canonical single-line values."
13966
- });
13967
- return;
13968
- }
13969
- if (value.includes("?") || value.includes("#")) {
13970
- context.addIssue({
13971
- code: "custom",
13972
- message: "Repository URLs must not contain a query or fragment."
13973
- });
13974
- return;
13975
- }
13976
- if (/^(?:[a-z\d._-]+@)?[a-z\d.-]+:[^\s/][^\s]*$/iu.test(value)) return;
13977
- try {
13978
- const parsed = new URL(value);
13979
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "ssh:" || !parsed.hostname || parsed.pathname.split("/").every((part) => part.length === 0) || parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) throw new Error("invalid transport");
13980
- } catch {
13981
- context.addIssue({
13982
- code: "custom",
13983
- message: "Repository URLs must use credential-free HTTP(S), ssh://, or scp-like SSH syntax."
13984
- });
14180
+ /**
14181
+ * Parse the input as a stream of YAML documents.
14182
+ *
14183
+ * Documents should be separated from each other by `...` or `---` marker lines.
14184
+ *
14185
+ * @returns If an empty `docs` array is returned, it will be of type
14186
+ * EmptyStream and contain additional stream information. In
14187
+ * TypeScript, you should use `'empty' in docs` as a type guard for it.
14188
+ */
14189
+ function parseAllDocuments(source, options = {}) {
14190
+ const { lineCounter, prettyErrors } = parseOptions(options);
14191
+ const parser$1 = new parser.Parser(lineCounter?.addNewLine);
14192
+ const composer$1 = new composer.Composer(options);
14193
+ const docs = Array.from(composer$1.compose(parser$1.parse(source)));
14194
+ if (prettyErrors && lineCounter) for (const doc of docs) {
14195
+ doc.errors.forEach(errors.prettifyError(source, lineCounter));
14196
+ doc.warnings.forEach(errors.prettifyError(source, lineCounter));
14197
+ }
14198
+ if (docs.length > 0) return docs;
14199
+ return Object.assign([], { empty: true }, composer$1.streamInfo());
13985
14200
  }
13986
- });
13987
- const treeNameSchema = string$2().superRefine((value, context) => {
13988
- if (!/^[A-Za-z\d][A-Za-z\d._-]{0,99}$/u.test(value) || /\.git$/iu.test(value)) context.addIssue({
13989
- code: "custom",
13990
- message: "Tree name must be a safe single path segment."
13991
- });
13992
- });
13993
- const githubRepositoryIdentitySchema = string$2().superRefine((value, context) => {
13994
- const parts = value.split("/");
13995
- const [owner, name] = parts;
13996
- if (parts.length !== 2 || owner === void 0 || name === void 0 || !/^[A-Za-z\d](?:[A-Za-z\d-]{0,37}[A-Za-z\d])?$/u.test(owner) || !treeNameSchema.safeParse(name).success) context.addIssue({
13997
- code: "custom",
13998
- message: "Repository must be an explicit GitHub OWNER/REPO identity."
13999
- });
14000
- });
14001
- const contextTreeRootNodeSchema = object({
14002
- frontmatter: object({
14003
- schemaVersion: literal(1),
14004
- title: string$2().trim().min(1),
14005
- description: string$2().trim().min(1).optional(),
14006
- soft_links: array(string$2().trim().min(1)).min(1).optional()
14007
- }).loose(),
14008
- body: string$2().trim().min(1, "Root NODE.md must contain repository-wide context.")
14009
- });
14010
- function parseContextTreeRootNode(markdown) {
14011
- if (Buffer.byteLength(markdown, "utf8") > 16384) throw new Error(`Root NODE.md exceeds the ${CONTEXT_TREE_ROOT_NODE_MAX_BYTES}-byte limit.`);
14012
- const document = parseMarkdownFrontmatter(markdown);
14013
- if (document.frontmatter === "missing") throw new Error("Root NODE.md must contain YAML frontmatter.");
14014
- if (document.frontmatter === "invalid") throw new Error(`Root NODE.md frontmatter is invalid: ${document.error}`);
14015
- return contextTreeRootNodeSchema.parse({
14016
- frontmatter: document.data,
14017
- body: document.body
14018
- });
14019
- }
14020
- const contextContentClassSchema = _enum([
14021
- "normal",
14022
- "member",
14023
- "repo-infra"
14024
- ]);
14025
- const treeValidationFindingSchema = object({
14026
- code: _enum(VALIDATION_CODES),
14027
- message: string$2(),
14028
- path: string$2(),
14029
- target: string$2().optional()
14030
- }).strict();
14031
- const contextContentClassCountsSchema = object({
14032
- normal: number().int().nonnegative(),
14033
- member: number().int().nonnegative(),
14034
- "repo-infra": number().int().nonnegative()
14035
- }).strict();
14036
- object({
14037
- content: string$2(),
14038
- schemaVersion: literal(1)
14039
- }).strict();
14040
- const contextTreeReadCommonFields = {
14041
- contentClass: contextContentClassSchema,
14042
- kind: _enum(["directory", "file"]),
14043
- path: string$2()
14044
- };
14045
- const contextTreeReadNodeSchema = object({
14046
- body: string$2(),
14047
- frontmatter: record(string$2(), unknown()),
14048
- ...contextTreeReadCommonFields
14049
- }).strict();
14050
- object({
14051
- children: array(object({
14052
- description: string$2().optional(),
14053
- title: string$2(),
14054
- ...contextTreeReadCommonFields
14055
- }).strict()),
14056
- node: contextTreeReadNodeSchema,
14057
- root: string$2(),
14058
- schemaVersion: literal(1),
14059
- target: string$2()
14060
- }).strict();
14061
- object({
14062
- findings: array(treeValidationFindingSchema),
14063
- ok: boolean(),
14064
- root: string$2(),
14065
- scannedByContentClass: contextContentClassCountsSchema,
14066
- schemaVersion: literal(1)
14067
- }).strict();
14068
- const contextTreeStateSchema = discriminatedUnion("kind", [object({
14069
- kind: literal("local"),
14070
- path: absoluteSingleLinePathSchema
14071
- }).strict(), object({
14072
- kind: literal("github"),
14073
- path: absoluteSingleLinePathSchema,
14074
- repository: githubRepositoryIdentitySchema
14075
- }).strict()]);
14076
- const contextTreeConnectionSchema = object({
14077
- projectPath: absoluteSingleLinePathSchema,
14078
- tree: contextTreeStateSchema
14079
- }).strict();
14080
- object({
14081
- schemaVersion: literal(1),
14082
- tree: contextTreeStateSchema
14083
- }).strict();
14084
- object({
14085
- branch: string$2().trim().min(1),
14086
- commitSha: string$2(),
14087
- created: boolean(),
14088
- schemaVersion: literal(1),
14089
- title: string$2().trim().min(1),
14090
- treePath: absoluteSingleLinePathSchema
14091
- }).strict();
14092
- const managedTreeListingEntrySchema = object({
14093
- name: treeNameSchema,
14094
- tree: contextTreeStateSchema
14095
- }).strict();
14096
- object({
14097
- schemaVersion: literal(1),
14098
- trees: array(managedTreeListingEntrySchema)
14099
- }).strict();
14100
- object({
14101
- branch: string$2().trim().min(1),
14102
- schemaVersion: literal(1),
14103
- sha: string$2(),
14104
- tree: contextTreeStateSchema
14105
- }).strict();
14106
- object({
14107
- schemaVersion: literal(1),
14108
- worktreePath: absoluteSingleLinePathSchema
14109
- }).strict();
14110
- object({
14111
- branch: string$2().trim().min(1),
14112
- schemaVersion: literal(1),
14113
- sha: string$2()
14114
- }).strict();
14115
- object({
14116
- branch: string$2().trim().min(1),
14117
- repository: githubRepositoryIdentitySchema,
14118
- schemaVersion: literal(1),
14119
- sha: string$2(),
14120
- url: credentialFreeRepositoryUrlSchema
14121
- }).strict();
14122
- object({
14123
- error: object({
14124
- code: _enum(CLI_ERROR_CODES),
14125
- message: string$2()
14126
- }).strict(),
14127
- ok: literal(false),
14128
- schemaVersion: literal(1)
14129
- }).strict();
14130
- //#endregion
14131
- //#region src/core/internal/errors.ts
14132
- /**
14133
- * A failure the CLI reports with a specific machine-readable code. Anything
14134
- * thrown as a plain Error is reported as CONTEXT_TREE_FAILED instead.
14135
- */
14136
- var ContextTreeError = class extends Error {
14137
- code;
14138
- constructor(code, message) {
14139
- super(message);
14140
- this.name = "ContextTreeError";
14141
- this.code = code;
14201
+ /** Parse an input string into a single YAML.Document */
14202
+ function parseDocument(source, options = {}) {
14203
+ const { lineCounter, prettyErrors } = parseOptions(options);
14204
+ const parser$1 = new parser.Parser(lineCounter?.addNewLine);
14205
+ const composer$1 = new composer.Composer(options);
14206
+ let doc = null;
14207
+ for (const _doc of composer$1.compose(parser$1.parse(source), true, source.length)) if (!doc) doc = _doc;
14208
+ else if (doc.options.logLevel !== "silent") {
14209
+ doc.errors.push(new errors.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
14210
+ break;
14211
+ }
14212
+ if (prettyErrors && lineCounter) {
14213
+ doc.errors.forEach(errors.prettifyError(source, lineCounter));
14214
+ doc.warnings.forEach(errors.prettifyError(source, lineCounter));
14215
+ }
14216
+ return doc;
14142
14217
  }
14143
- };
14144
- //#endregion
14145
- //#region src/core/internal/git.ts
14146
- function defaultRunner(command, args) {
14147
- const result = spawnSync(command, args, {
14148
- encoding: "utf8",
14149
- stdio: [
14150
- "ignore",
14151
- "pipe",
14152
- "pipe"
14153
- ]
14154
- });
14155
- return {
14156
- status: result.status,
14157
- stderr: typeof result.stderr === "string" ? result.stderr : "",
14158
- stdout: typeof result.stdout === "string" ? result.stdout : ""
14159
- };
14160
- }
14161
- /** A failed Git or `gh` operation. Messages never include the argv. */
14162
- var CommandError = class extends Error {
14163
- command;
14164
- status;
14165
- stderr;
14166
- constructor(command, status, stderr, message) {
14167
- const detail = sanitizeCommandOutput(stderr).trim();
14168
- super(detail.length > 0 ? `${message}: ${detail}` : message);
14169
- this.name = "CommandError";
14170
- this.command = command;
14171
- this.status = status;
14172
- this.stderr = detail;
14218
+ function parse(src, reviver, options) {
14219
+ let _reviver = void 0;
14220
+ if (typeof reviver === "function") _reviver = reviver;
14221
+ else if (options === void 0 && reviver && typeof reviver === "object") options = reviver;
14222
+ const doc = parseDocument(src, options);
14223
+ if (!doc) return null;
14224
+ doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
14225
+ if (doc.errors.length > 0) if (doc.options.logLevel !== "silent") throw doc.errors[0];
14226
+ else doc.errors = [];
14227
+ return doc.toJS(Object.assign({ reviver: _reviver }, options));
14173
14228
  }
14174
- };
14175
- /** Remove credentials and common access-token shapes before surfacing subprocess output. */
14176
- function sanitizeCommandOutput(value) {
14177
- return value.replace(/((?:https?|ssh):\/\/)[^\s/@]+@/giu, "$1<redacted>@").replace(/\b(?:gh[opsu]_[A-Za-z\d_]{20,}|github_pat_[A-Za-z\d_]{20,})\b/gu, "<redacted>").replace(/(authorization\s*:\s*(?:bearer|token)\s+)[^\s]+/giu, "$1<redacted>");
14178
- }
14179
- function trimOutput(value) {
14180
- return value.trim();
14181
- }
14182
- function execute(runner, command, args, message) {
14183
- const result = runner(command, args);
14184
- if (result.status !== 0) throw new CommandError(command, result.status, result.stderr, message);
14185
- return trimOutput(result.stdout);
14186
- }
14187
- /** Run a Git command that is not scoped by `-C`, such as `git init <path>`. */
14188
- function gitCommand(args, options = {}) {
14189
- return execute(options.runner ?? defaultRunner, "git", args, options.message ?? "A Git operation failed.");
14190
- }
14191
- /** Run `git -C <root> <args>` and return trimmed stdout, throwing on failure. */
14192
- function git(root, args, options = {}) {
14193
- return execute(options.runner ?? defaultRunner, "git", [
14194
- "-C",
14195
- root,
14196
- ...args
14197
- ], options.message ?? "A Git operation failed.");
14198
- }
14199
- /** Run `git -C <root> <args>` and return trimmed stdout, or undefined on failure. */
14200
- function optionalGit(root, args, runner = defaultRunner) {
14201
- const result = runner("git", [
14202
- "-C",
14203
- root,
14204
- ...args
14205
- ]);
14206
- if (result.status !== 0) return void 0;
14207
- return trimOutput(result.stdout);
14208
- }
14209
- /** Run `gh <args>` and return trimmed stdout, throwing on failure. */
14210
- function gh(args, options = {}) {
14211
- return execute(options.runner ?? defaultRunner, "gh", args, options.message ?? "A GitHub CLI operation failed.");
14212
- }
14213
- //#endregion
14214
- //#region src/core/internal/github-repository.ts
14215
- const ORIGIN_MESSAGE = "Context Tree origin must identify a credential-free GitHub OWNER/REPO repository.";
14216
- function canonicalGitHubRepositoryUrl(repository) {
14217
- githubRepositoryIdentitySchema.parse(repository);
14218
- return `https://github.com/${repository}.git`;
14219
- }
14220
- /** Derive OWNER/REPO from a github.com origin, rejecting anything else. */
14221
- function gitHubRepositoryFromOriginUrl(origin) {
14222
- if (!credentialFreeRepositoryUrlSchema.safeParse(origin).success) throw new Error(ORIGIN_MESSAGE);
14223
- let owner;
14224
- let name;
14225
- const scp = /^(?:git@)?github\.com:([^/]+)\/(.+)$/iu.exec(origin);
14226
- if (scp !== null) [, owner, name] = scp;
14227
- else {
14228
- const url = URL.parse(origin);
14229
- if (url === null || url.hostname.toLowerCase() !== "github.com") throw new Error(ORIGIN_MESSAGE);
14230
- [owner, name] = url.pathname.replace(/^\/+|\/+$/gu, "").split("/");
14229
+ function stringify(value, replacer, options) {
14230
+ let _replacer = null;
14231
+ if (typeof replacer === "function" || Array.isArray(replacer)) _replacer = replacer;
14232
+ else if (options === void 0 && replacer) options = replacer;
14233
+ if (typeof options === "string") options = options.length;
14234
+ if (typeof options === "number") {
14235
+ const indent = Math.round(options);
14236
+ options = indent < 1 ? void 0 : indent > 8 ? { indent: 8 } : { indent };
14237
+ }
14238
+ if (value === void 0) {
14239
+ const { keepUndefined } = options ?? replacer ?? {};
14240
+ if (!keepUndefined) return void 0;
14241
+ }
14242
+ if (identity.isDocument(value) && !_replacer) return value.toString(options);
14243
+ return new Document.Document(value, _replacer, options).toString(options);
14231
14244
  }
14232
- const repository = `${owner ?? ""}/${(name ?? "").replace(/\.git$/iu, "")}`;
14233
- if (!githubRepositoryIdentitySchema.safeParse(repository).success) throw new Error(ORIGIN_MESSAGE);
14234
- return repository;
14245
+ exports.parse = parse;
14246
+ exports.parseAllDocuments = parseAllDocuments;
14247
+ exports.parseDocument = parseDocument;
14248
+ exports.stringify = stringify;
14249
+ }));
14250
+ //#endregion
14251
+ //#region src/internal/value.ts
14252
+ var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
14253
+ var composer = require_composer();
14254
+ var Document = require_Document();
14255
+ var Schema = require_Schema();
14256
+ var errors = require_errors();
14257
+ var Alias = require_Alias();
14258
+ var identity = require_identity();
14259
+ var Pair = require_Pair();
14260
+ var Scalar = require_Scalar();
14261
+ var YAMLMap = require_YAMLMap();
14262
+ var YAMLSeq = require_YAMLSeq();
14263
+ require_cst();
14264
+ var lexer = require_lexer();
14265
+ var lineCounter = require_line_counter();
14266
+ var parser = require_parser();
14267
+ var publicApi = require_public_api();
14268
+ var visit = require_visit();
14269
+ exports.Composer = composer.Composer;
14270
+ exports.Document = Document.Document;
14271
+ exports.Schema = Schema.Schema;
14272
+ exports.YAMLError = errors.YAMLError;
14273
+ exports.YAMLParseError = errors.YAMLParseError;
14274
+ exports.YAMLWarning = errors.YAMLWarning;
14275
+ exports.Alias = Alias.Alias;
14276
+ exports.isAlias = identity.isAlias;
14277
+ exports.isCollection = identity.isCollection;
14278
+ exports.isDocument = identity.isDocument;
14279
+ exports.isMap = identity.isMap;
14280
+ exports.isNode = identity.isNode;
14281
+ exports.isPair = identity.isPair;
14282
+ exports.isScalar = identity.isScalar;
14283
+ exports.isSeq = identity.isSeq;
14284
+ exports.Pair = Pair.Pair;
14285
+ exports.Scalar = Scalar.Scalar;
14286
+ exports.YAMLMap = YAMLMap.YAMLMap;
14287
+ exports.YAMLSeq = YAMLSeq.YAMLSeq;
14288
+ exports.Lexer = lexer.Lexer;
14289
+ exports.LineCounter = lineCounter.LineCounter;
14290
+ exports.Parser = parser.Parser;
14291
+ exports.parse = publicApi.parse;
14292
+ exports.parseAllDocuments = publicApi.parseAllDocuments;
14293
+ exports.parseDocument = publicApi.parseDocument;
14294
+ exports.stringify = publicApi.stringify;
14295
+ exports.visit = visit.visit;
14296
+ exports.visitAsync = visit.visitAsync;
14297
+ })))();
14298
+ function isRecord(value) {
14299
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14235
14300
  }
14236
14301
  //#endregion
14237
- //#region src/core/path.ts
14238
- function isPathInside(root, target) {
14239
- const path = relative(root, target);
14240
- return path === "" || !path.startsWith("..") && !isAbsolute(path);
14302
+ //#region src/internal/frontmatter.ts
14303
+ function parseYamlMapping(source) {
14304
+ const value = (0, import_dist.parse)(source);
14305
+ if (!isRecord(value)) throw new Error("frontmatter must be a YAML mapping");
14306
+ return value;
14241
14307
  }
14242
- function resolveTreeRoot(path) {
14243
- const absolute = resolve(path);
14244
- const entry = lstatSync(absolute);
14245
- if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
14246
- return realpathSync(absolute);
14308
+ function readLine(source, start) {
14309
+ const newline = source.indexOf("\n", start);
14310
+ const end = newline === -1 ? source.length : newline + 1;
14311
+ const contentEnd = newline === -1 ? source.length : source.charCodeAt(newline - 1) === 13 ? newline - 1 : newline;
14312
+ return {
14313
+ end,
14314
+ start,
14315
+ value: source.slice(start, contentEnd)
14316
+ };
14247
14317
  }
14248
- /** Resolve a directory while rejecting symlinks in user-controlled path components. */
14249
- function realDirectoryWithoutSymlinks(path, label) {
14250
- const absolute = resolve(path);
14251
- const parsed = parse(absolute);
14252
- const parts = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
14253
- let current = parsed.root;
14254
- for (const [index, part] of parts.entries()) {
14255
- current = resolve(current, part);
14256
- if (lstatSync(current).isSymbolicLink()) if (index === 0) current = realpathSync(current);
14257
- else throw new Error(`${label} must contain no symlink component.`);
14318
+ function parseMarkdownFrontmatter(source) {
14319
+ const opening = readLine(source, 0);
14320
+ if (opening.value !== "---") return {
14321
+ body: source,
14322
+ data: null,
14323
+ frontmatter: "missing"
14324
+ };
14325
+ let closing;
14326
+ let lineStart = opening.end;
14327
+ while (lineStart < source.length) {
14328
+ const line = readLine(source, lineStart);
14329
+ if (line.value === "---") {
14330
+ closing = line;
14331
+ break;
14332
+ }
14333
+ lineStart = line.end;
14334
+ }
14335
+ if (closing === void 0) return {
14336
+ body: "",
14337
+ data: null,
14338
+ error: "frontmatter closing delimiter is missing",
14339
+ frontmatter: "invalid"
14340
+ };
14341
+ const body = source.slice(closing.end);
14342
+ try {
14343
+ return {
14344
+ body,
14345
+ data: parseYamlMapping(source.slice(opening.end, closing.start)),
14346
+ frontmatter: "valid"
14347
+ };
14348
+ } catch (error) {
14349
+ return {
14350
+ body,
14351
+ data: null,
14352
+ error: error instanceof Error ? error.message : String(error),
14353
+ frontmatter: "invalid"
14354
+ };
14258
14355
  }
14259
- if (!lstatSync(current).isDirectory()) throw new Error(`${label} must be a directory.`);
14260
- return realpathSync(current);
14261
14356
  }
14262
- function toPosixPath(path) {
14263
- return path.replace(/\\/gu, "/");
14357
+ //#endregion
14358
+ //#region src/core/internal/filesystem.ts
14359
+ function readUtf8File(path) {
14360
+ return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
14264
14361
  }
14265
14362
  //#endregion
14266
- //#region src/core/internal/project.ts
14363
+ //#region src/core/internal/root-node.ts
14267
14364
  /**
14268
- * Projects are identified solely by their canonical local root. A Git
14269
- * repository without an origin, a non-Git directory, a Git worktree, and a
14270
- * separate clone are all independent checkouts with their own canonical root.
14365
+ * The one root NODE.md reader. Verification and tree-state resolution both need
14366
+ * it, and they must agree on the fail-closed file guard, so it lives here rather
14367
+ * than in either caller: tree-state already imports verify, so a shared helper
14368
+ * in verify would close a cycle.
14271
14369
  */
14272
- function canonicalProjectRoot(path, runner) {
14273
- const directory = realDirectoryWithoutSymlinks(path, "Project path");
14274
- const toplevel = optionalGit(directory, ["rev-parse", "--show-toplevel"], runner);
14275
- if (toplevel === void 0 || toplevel.length === 0) return directory;
14276
- return realpathSync(toplevel);
14370
+ /** Parse root NODE.md content, bounding size before any YAML or Markdown work. */
14371
+ function parseContextTreeRootNode(markdown) {
14372
+ if (Buffer.byteLength(markdown, "utf8") > 16384) throw new Error(`Root NODE.md exceeds the ${CONTEXT_TREE_ROOT_NODE_MAX_BYTES}-byte limit.`);
14373
+ const document = parseMarkdownFrontmatter(markdown);
14374
+ if (document.frontmatter === "missing") throw new Error("Root NODE.md must contain YAML frontmatter.");
14375
+ if (document.frontmatter === "invalid") throw new Error(`Root NODE.md frontmatter is invalid: ${document.error}`);
14376
+ return contextTreeRootNodeSchema.parse({
14377
+ frontmatter: document.data,
14378
+ body: document.body
14379
+ });
14277
14380
  }
14278
- //#endregion
14279
- //#region src/core/internal/filesystem.ts
14280
- function readUtf8File(path) {
14281
- return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
14381
+ /** Read and parse a tree's root NODE.md, refusing symlinked or irregular files. */
14382
+ function readRootNode(root) {
14383
+ const path = join(root, "NODE.md");
14384
+ const entry = lstatSync(path);
14385
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Root NODE.md must be a regular file and must not be a symlink.");
14386
+ return parseContextTreeRootNode(readUtf8File(path));
14282
14387
  }
14283
14388
  //#endregion
14284
14389
  //#region src/core/internal/content-class.ts
@@ -25264,16 +25369,13 @@ function collectNodeValidationFindings(treeRoot) {
25264
25369
  //#endregion
25265
25370
  //#region src/core/verify.ts
25266
25371
  function rootNodeFindings(root) {
25267
- const path = join(root, "NODE.md");
25268
- if (!existsSync(path)) return [{
25372
+ if (!existsSync(join(root, "NODE.md"))) return [{
25269
25373
  code: VALIDATION_CODES.rootMissing,
25270
25374
  message: "root NODE.md is missing",
25271
25375
  path: "NODE.md"
25272
25376
  }];
25273
25377
  try {
25274
- const entry = lstatSync(path);
25275
- if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Root NODE.md must be a regular file and must not be a symlink.");
25276
- parseContextTreeRootNode(readUtf8File(path));
25378
+ readRootNode(root);
25277
25379
  return [];
25278
25380
  } catch (error) {
25279
25381
  return [{
@@ -25320,13 +25422,6 @@ function exactGitRoot(treePath, runner) {
25320
25422
  if (realpathSync(toplevel) !== root) throw new Error("Context Tree path must be the real Git root.");
25321
25423
  return root;
25322
25424
  }
25323
- /** Parse the root NODE.md, refusing symlinked or irregular files. */
25324
- function parseRootNode(root) {
25325
- const path = join(root, "NODE.md");
25326
- const entry = lstatSync(path);
25327
- if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Context Tree root NODE.md must be a regular file.");
25328
- return parseContextTreeRootNode(readUtf8File(path));
25329
- }
25330
25425
  /**
25331
25426
  * Validate a clean checkout without inferring state from mutable Git remotes.
25332
25427
  * Uncommitted changes and invalid content each get their own code so callers
@@ -25365,7 +25460,7 @@ const connectionsFileSchema = object({
25365
25460
  }).strict();
25366
25461
  const DUPLICATE_MESSAGE = "Duplicate Context Tree connection records exist for this project.";
25367
25462
  const NO_CONNECTION_MESSAGE = "No Context Tree connection exists for this project; run context-tree create or connect.";
25368
- function realHome() {
25463
+ function realHome$1() {
25369
25464
  try {
25370
25465
  return realpathSync(homedir());
25371
25466
  } catch {
@@ -25374,7 +25469,7 @@ function realHome() {
25374
25469
  }
25375
25470
  /** Create a managed application directory below the home directory, failing closed on symlinks. */
25376
25471
  function ensureManagedDirectory(...segments) {
25377
- let current = realHome();
25472
+ let current = realHome$1();
25378
25473
  for (const segment of segments) {
25379
25474
  current = join(current, segment);
25380
25475
  const entry = lstatSync(current, { throwIfNoEntry: false });
@@ -25388,10 +25483,10 @@ function ensureManagedDirectory(...segments) {
25388
25483
  }
25389
25484
  /** The managed namespace without creating it; listing must not create an absent directory. */
25390
25485
  function managedTreesPath() {
25391
- return join(realHome(), ".context-tree", "trees");
25486
+ return join(realHome$1(), ".context-tree", "trees");
25392
25487
  }
25393
25488
  function connectionsPath() {
25394
- return join(realHome(), ".context-tree", "connections.json");
25489
+ return join(realHome$1(), ".context-tree", "connections.json");
25395
25490
  }
25396
25491
  function managedTreesRoot() {
25397
25492
  return ensureManagedDirectory(".context-tree", "trees");
@@ -25565,24 +25660,26 @@ function realManagedDirectory(name, destination) {
25565
25660
  * clean, fully valid Git checkout at an explicit disk path in place.
25566
25661
  */
25567
25662
  function connectProject(options, runner) {
25568
- if ("treePath" in options) {
25569
- const tree = classifyCheckout(options.treePath, runner);
25570
- return upsertConnection({
25663
+ /** Store the connection, then record it in the project so any agent can find it. */
25664
+ const connect = (tree) => {
25665
+ const result = upsertConnection({
25571
25666
  projectPath: options.projectPath,
25572
25667
  tree
25573
25668
  }, runner);
25574
- }
25669
+ return {
25670
+ pointer: writeProjectPointer(canonicalProjectRoot(options.projectPath, runner), result.tree.path),
25671
+ schemaVersion: 1,
25672
+ tree: result.tree
25673
+ };
25674
+ };
25675
+ if ("treePath" in options) return connect(classifyCheckout(options.treePath, runner));
25575
25676
  const treesRoot = managedTreesRoot();
25576
25677
  if (!options.target.includes("/")) {
25577
25678
  const name = managedName(options.target);
25578
25679
  const destination = join(treesRoot, name);
25579
25680
  if (!existsSync(destination)) throw new Error(`No managed Context Tree named ${name} exists.`);
25580
25681
  realManagedDirectory(name, destination);
25581
- const tree = classifyCheckout(destination, runner);
25582
- return upsertConnection({
25583
- projectPath: options.projectPath,
25584
- tree
25585
- }, runner);
25682
+ return connect(classifyCheckout(destination, runner));
25586
25683
  }
25587
25684
  const repository = githubRepositoryIdentitySchema.parse(options.target);
25588
25685
  const repositoryName = repository.split("/")[1];
@@ -25593,10 +25690,7 @@ function connectProject(options, runner) {
25593
25690
  realManagedDirectory(name, destination);
25594
25691
  const tree = classifyCheckout(destination, runner);
25595
25692
  if (tree.kind !== "github" || !sameRepository(tree.repository, repository)) throw new Error(`Managed Context Tree name ${name} is already used by a different tree.`);
25596
- return upsertConnection({
25597
- projectPath: options.projectPath,
25598
- tree
25599
- }, runner);
25693
+ return connect(tree);
25600
25694
  }
25601
25695
  mkdirSync(destination, { mode: 448 });
25602
25696
  try {
@@ -25614,10 +25708,7 @@ function connectProject(options, runner) {
25614
25708
  });
25615
25709
  const tree = classifyCheckout(destination, runner);
25616
25710
  if (tree.kind !== "github" || !sameRepository(tree.repository, repository)) throw new Error("The cloned Context Tree origin does not match the requested repository.");
25617
- return upsertConnection({
25618
- projectPath: options.projectPath,
25619
- tree
25620
- }, runner);
25711
+ return connect(tree);
25621
25712
  } catch (error) {
25622
25713
  rmSync(destination, {
25623
25714
  force: true,
@@ -25768,7 +25859,7 @@ function projectName(canonicalRoot) {
25768
25859
  const normalized = basename(canonicalRoot).toLowerCase().replace(/[^a-z\d._-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^[-.]+/u, "").replace(/[-.]+$/u, "").slice(0, 40);
25769
25860
  return /^[a-z\d]/u.test(normalized) ? normalized : "project";
25770
25861
  }
25771
- function existingCreateResult(destination, runner) {
25862
+ function existingCreateResult(canonical, destination, runner) {
25772
25863
  return {
25773
25864
  branch: git(destination, [
25774
25865
  "symbolic-ref",
@@ -25783,8 +25874,9 @@ function existingCreateResult(destination, runner) {
25783
25874
  runner
25784
25875
  }),
25785
25876
  created: false,
25877
+ pointer: writeProjectPointer(canonical, destination),
25786
25878
  schemaVersion: 1,
25787
- title: parseRootNode(destination).frontmatter.title,
25879
+ title: readRootNode(destination).frontmatter.title,
25788
25880
  treePath: destination
25789
25881
  };
25790
25882
  }
@@ -25798,7 +25890,7 @@ function createProject(projectPath, runner) {
25798
25890
  if (existsSync(destination)) {
25799
25891
  const entry = lstatSync(destination);
25800
25892
  if (entry.isSymbolicLink() || !entry.isDirectory() || current === void 0) throw new Error(`Managed Context Tree name ${name} is occupied; run context-tree connect ${name}.`);
25801
- return existingCreateResult(destination, runner);
25893
+ return existingCreateResult(canonical, destination, runner);
25802
25894
  }
25803
25895
  mkdirSync(destination, { mode: 448 });
25804
25896
  try {
@@ -25818,6 +25910,7 @@ function createProject(projectPath, runner) {
25818
25910
  branch: scaffold.branch,
25819
25911
  commitSha: scaffold.commit,
25820
25912
  created: true,
25913
+ pointer: writeProjectPointer(canonical, scaffold.root),
25821
25914
  schemaVersion: 1,
25822
25915
  title: name,
25823
25916
  treePath: scaffold.root
@@ -25831,6 +25924,115 @@ function createProject(projectPath, runner) {
25831
25924
  }
25832
25925
  }
25833
25926
  //#endregion
25927
+ //#region src/core/install.ts
25928
+ /** Per-host configuration directory, relative to the home directory or to a project root. */
25929
+ const HOST_CONFIG_DIRECTORY = {
25930
+ claude: ".claude",
25931
+ codex: ".codex"
25932
+ };
25933
+ /** Every supported host keeps user skills in the same subdirectory of its configuration directory. */
25934
+ const SKILLS_DIRECTORY = "skills";
25935
+ /** Only directories carrying this prefix are ever replaced or removed. */
25936
+ const OWNED_SKILL_PREFIX = "context-tree-";
25937
+ function realHome() {
25938
+ try {
25939
+ return realpathSync(homedir());
25940
+ } catch {
25941
+ return homedir();
25942
+ }
25943
+ }
25944
+ /** Create a directory below `root`, failing closed on symlinks and non-directories. */
25945
+ function ensureRealDirectory(root, segments) {
25946
+ let current = root;
25947
+ for (const segment of segments) {
25948
+ current = join(current, segment);
25949
+ const entry = lstatSync(current, { throwIfNoEntry: false });
25950
+ if (entry === void 0) {
25951
+ mkdirSync(current, { mode: 448 });
25952
+ continue;
25953
+ }
25954
+ if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree skill directory must be a real directory: ${current}`);
25955
+ }
25956
+ return current;
25957
+ }
25958
+ /** Copy a packaged skill tree, refusing to read or write through symlinks. Skills are never executable. */
25959
+ function copyRealTree(source, destination) {
25960
+ const entry = lstatSync(source);
25961
+ if (entry.isSymbolicLink()) throw new Error(`Refusing to install a symlinked skill entry: ${source}`);
25962
+ if (entry.isDirectory()) {
25963
+ mkdirSync(destination, {
25964
+ mode: 448,
25965
+ recursive: true
25966
+ });
25967
+ for (const child of readdirSync(source)) copyRealTree(join(source, child), join(destination, child));
25968
+ return;
25969
+ }
25970
+ if (!entry.isFile()) throw new Error(`Refusing to install a non-regular skill entry: ${source}`);
25971
+ copyFileSync(source, destination);
25972
+ chmodSync(destination, 420);
25973
+ }
25974
+ /** Packaged skill directory names, e.g. `context-tree-read`. */
25975
+ function packagedSkillNames(skillsRoot) {
25976
+ return readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith(OWNED_SKILL_PREFIX)).map((entry) => entry.name).sort();
25977
+ }
25978
+ /** Resolve one host's destination, or the reason it was skipped. */
25979
+ function hostDestination(host, root, isProjectInstall) {
25980
+ const configDirectory = HOST_CONFIG_DIRECTORY[host];
25981
+ if (!isProjectInstall) {
25982
+ const hostRoot = join(root, configDirectory);
25983
+ const entry = lstatSync(hostRoot, { throwIfNoEntry: false });
25984
+ if (entry === void 0) return { reason: `${hostRoot} does not exist; install ${host} first, then run context-tree install.` };
25985
+ if (entry.isSymbolicLink() || !entry.isDirectory()) return { reason: `${hostRoot} is not a real directory.` };
25986
+ }
25987
+ return { destination: ensureRealDirectory(root, [configDirectory, SKILLS_DIRECTORY]) };
25988
+ }
25989
+ /**
25990
+ * Copy the packaged skills into each requested host's skill directory.
25991
+ *
25992
+ * Existing `context-tree-*` directories are replaced; that is the upgrade path. Skill
25993
+ * directories the package does not own are never touched. A project install always
25994
+ * creates its target, because the caller named it.
25995
+ */
25996
+ function installSkills(options = {}) {
25997
+ const skillsRoot = resolvePackagedResource("skills");
25998
+ const skills = packagedSkillNames(skillsRoot);
25999
+ if (skills.length === 0) throw new Error("Packaged skills are missing; reinstall @first-tree-ai/context-tree.");
26000
+ const hosts = options.hosts === void 0 || options.hosts.length === 0 ? SKILL_HOSTS : options.hosts;
26001
+ const projectRoot = options.projectPath === void 0 ? void 0 : resolve(options.projectPath);
26002
+ const root = projectRoot ?? realHome();
26003
+ const installed = [];
26004
+ const skipped = [];
26005
+ for (const host of hosts) {
26006
+ const resolved = hostDestination(host, root, projectRoot !== void 0);
26007
+ if ("reason" in resolved) {
26008
+ skipped.push({
26009
+ host,
26010
+ reason: resolved.reason
26011
+ });
26012
+ continue;
26013
+ }
26014
+ for (const skill of skills) {
26015
+ const target = join(resolved.destination, skill);
26016
+ if (lstatSync(target, { throwIfNoEntry: false }) !== void 0) rmSync(target, {
26017
+ force: true,
26018
+ recursive: true
26019
+ });
26020
+ copyRealTree(join(skillsRoot, skill), target);
26021
+ }
26022
+ installed.push({
26023
+ host,
26024
+ path: resolved.destination,
26025
+ skills
26026
+ });
26027
+ }
26028
+ return {
26029
+ installed,
26030
+ schemaVersion: 1,
26031
+ skipped,
26032
+ version: readPackageVersion()
26033
+ };
26034
+ }
26035
+ //#endregion
25834
26036
  //#region src/core/publish.ts
25835
26037
  function authenticatedAccount(runner) {
25836
26038
  let login;
@@ -25922,6 +26124,76 @@ function publishProject(projectPath, options = {}, runner) {
25922
26124
  };
25923
26125
  }
25924
26126
  //#endregion
26127
+ //#region src/core/read.ts
26128
+ function normalizeTreeTarget(value) {
26129
+ if (!value || value === ".") return "";
26130
+ const normalized = posix.normalize(toPosixPath(value).replace(/^\.\//u, ""));
26131
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) throw new Error(`Read target is outside the Context Tree: ${value}`);
26132
+ return normalized.replace(/\/$/u, "");
26133
+ }
26134
+ function canonicalTarget(root, path) {
26135
+ const requested = normalizeTreeTarget(path);
26136
+ const semanticPath = requested === "NODE.md" ? "" : requested.endsWith("/NODE.md") ? dirname(requested) : requested;
26137
+ if (classifyContextContent(semanticPath) === "repo-infra") throw new Error(`Read target is repository infrastructure: ${requested || "."}`);
26138
+ const absolutePath = resolve(root, semanticPath);
26139
+ if (!isPathInside(root, absolutePath)) throw new Error("Read target escapes the Context Tree root.");
26140
+ const entry = lstatSync(absolutePath);
26141
+ if (entry.isSymbolicLink() || !entry.isDirectory() && !entry.isFile()) throw new Error(`Read target must be a real file or directory: ${requested || "."}`);
26142
+ if (realpathSync(absolutePath) !== absolutePath) throw new Error(`Read target must not traverse a symlink: ${requested || "."}`);
26143
+ const relativePath = toPosixPath(relative(root, absolutePath));
26144
+ if (entry.isFile() && !absolutePath.endsWith(".md")) throw new Error(`Read target must be a Markdown file or indexed directory: ${requested || "."}`);
26145
+ return {
26146
+ absolutePath,
26147
+ kind: entry.isDirectory() ? "directory" : "file",
26148
+ relativePath
26149
+ };
26150
+ }
26151
+ function readNode(path, relativePath, kind) {
26152
+ const documentPath = kind === "directory" ? join(path, "NODE.md") : path;
26153
+ const entry = lstatSync(documentPath);
26154
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Context Tree document must be a regular file: ${relativePath || "NODE.md"}`);
26155
+ const document = readNodeDocument(documentPath);
26156
+ if (document === null) throw new Error(`Context Tree document has invalid or missing metadata: ${relativePath || "."}`);
26157
+ return {
26158
+ body: document.body,
26159
+ contentClass: classifyContextContent(relativePath),
26160
+ frontmatter: document.frontmatter,
26161
+ kind,
26162
+ path: relativePath || "."
26163
+ };
26164
+ }
26165
+ function childSummary(root, parentPath, name) {
26166
+ const absolutePath = join(parentPath, name);
26167
+ const relativePath = toPosixPath(relative(root, absolutePath));
26168
+ const contentClass = classifyContextContent(relativePath);
26169
+ if (contentClass === "repo-infra") return null;
26170
+ const entry = lstatSync(absolutePath);
26171
+ if (entry.isSymbolicLink()) return null;
26172
+ const kind = entry.isDirectory() ? "directory" : entry.isFile() && name.endsWith(".md") && name !== "NODE.md" ? "file" : null;
26173
+ if (kind === null) return null;
26174
+ const document = readNodeDocument(kind === "directory" ? join(absolutePath, "NODE.md") : absolutePath);
26175
+ if (document === null) throw new Error(`Context Tree child has invalid or missing metadata: ${relativePath}`);
26176
+ return {
26177
+ contentClass,
26178
+ ...document.description === void 0 ? {} : { description: document.description },
26179
+ kind,
26180
+ path: relativePath,
26181
+ title: document.title
26182
+ };
26183
+ }
26184
+ function readTree(treePath, path) {
26185
+ const root = resolveTreeRoot(treePath);
26186
+ const target = canonicalTarget(root, path);
26187
+ const node = readNode(target.absolutePath, target.relativePath, target.kind);
26188
+ return {
26189
+ children: target.kind === "file" ? [] : readdirSync(target.absolutePath).map((name) => childSummary(root, target.absolutePath, name)).filter((child) => child !== null).sort((left, right) => left.path.localeCompare(right.path)),
26190
+ node,
26191
+ root,
26192
+ schemaVersion: 1,
26193
+ target: target.relativePath || "."
26194
+ };
26195
+ }
26196
+ //#endregion
25925
26197
  //#region src/core/sync.ts
25926
26198
  /**
25927
26199
  * Local trees report their checked-out state without network access. GitHub
@@ -25963,10 +26235,13 @@ function syncProject(projectPath, runner) {
25963
26235
  //#endregion
25964
26236
  //#region src/core/write.ts
25965
26237
  const TASK_BRANCH_PREFIX = "context-tree/write/";
26238
+ /** A prepared worktree left untouched for longer than this is treated as abandoned. */
26239
+ const ABANDONED_WRITE_AGE_MS = 1440 * 60 * 1e3;
25966
26240
  /** Synchronize first, then create an isolated task worktree at the exact HEAD. */
25967
26241
  function prepareContextWrite(projectPath, runner) {
25968
26242
  const synchronized = syncProject(projectPath, runner);
25969
26243
  const root = synchronized.tree.path;
26244
+ reclaimAbandonedWrites(root, synchronized.branch, runner);
25970
26245
  const destination = mkdtempSync(join(tmpdir(), "context-tree-write-"));
25971
26246
  const taskBranch = `${TASK_BRANCH_PREFIX}${basename(destination)}`;
25972
26247
  try {
@@ -26108,83 +26383,81 @@ function removeWorktree(root, worktreePath, taskBranch, runner) {
26108
26383
  runner
26109
26384
  });
26110
26385
  }
26111
- //#endregion
26112
- //#region src/core/policy.ts
26113
- function readContextTreePolicy() {
26114
- return {
26115
- content: readFileSync(resolvePackagedResource("policy", "context-tree-policy.md"), "utf8"),
26116
- schemaVersion: 1
26117
- };
26118
- }
26119
- //#endregion
26120
- //#region src/core/read.ts
26121
- function normalizeTreeTarget(value) {
26122
- if (!value || value === ".") return "";
26123
- const normalized = posix.normalize(toPosixPath(value).replace(/^\.\//u, ""));
26124
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) throw new Error(`Read target is outside the Context Tree: ${value}`);
26125
- return normalized.replace(/\/$/u, "");
26126
- }
26127
- function canonicalTarget(root, path) {
26128
- const requested = normalizeTreeTarget(path);
26129
- const semanticPath = requested === "NODE.md" ? "" : requested.endsWith("/NODE.md") ? dirname(requested) : requested;
26130
- if (classifyContextContent(semanticPath) === "repo-infra") throw new Error(`Read target is repository infrastructure: ${requested || "."}`);
26131
- const absolutePath = resolve(root, semanticPath);
26132
- if (!isPathInside(root, absolutePath)) throw new Error("Read target escapes the Context Tree root.");
26133
- const entry = lstatSync(absolutePath);
26134
- if (entry.isSymbolicLink() || !entry.isDirectory() && !entry.isFile()) throw new Error(`Read target must be a real file or directory: ${requested || "."}`);
26135
- if (realpathSync(absolutePath) !== absolutePath) throw new Error(`Read target must not traverse a symlink: ${requested || "."}`);
26136
- const relativePath = toPosixPath(relative(root, absolutePath));
26137
- if (entry.isFile() && !absolutePath.endsWith(".md")) throw new Error(`Read target must be a Markdown file or indexed directory: ${requested || "."}`);
26138
- return {
26139
- absolutePath,
26140
- kind: entry.isDirectory() ? "directory" : "file",
26141
- relativePath
26142
- };
26143
- }
26144
- function readNode(path, relativePath, kind) {
26145
- const documentPath = kind === "directory" ? join(path, "NODE.md") : path;
26146
- const entry = lstatSync(documentPath);
26147
- if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Context Tree document must be a regular file: ${relativePath || "NODE.md"}`);
26148
- const document = readNodeDocument(documentPath);
26149
- if (document === null) throw new Error(`Context Tree document has invalid or missing metadata: ${relativePath || "."}`);
26150
- return {
26151
- body: document.body,
26152
- contentClass: classifyContextContent(relativePath),
26153
- frontmatter: document.frontmatter,
26154
- kind,
26155
- path: relativePath || "."
26156
- };
26386
+ /** Map every reserved write branch that still has a registered worktree to its path. */
26387
+ function listWriteWorktrees(root, runner) {
26388
+ const paths = /* @__PURE__ */ new Map();
26389
+ const output = optionalGit(root, [
26390
+ "worktree",
26391
+ "list",
26392
+ "--porcelain"
26393
+ ], runner);
26394
+ if (output === void 0) return paths;
26395
+ let path;
26396
+ for (const record of output.split("\n")) {
26397
+ if (record.startsWith("worktree ")) {
26398
+ path = record.slice(9).trim();
26399
+ continue;
26400
+ }
26401
+ if (!record.startsWith("branch refs/heads/")) continue;
26402
+ const branch = record.slice(18).trim();
26403
+ if (path !== void 0 && branch.startsWith(TASK_BRANCH_PREFIX)) paths.set(branch, path);
26404
+ }
26405
+ return paths;
26157
26406
  }
26158
- function childSummary(root, parentPath, name) {
26159
- const absolutePath = join(parentPath, name);
26160
- const relativePath = toPosixPath(relative(root, absolutePath));
26161
- const contentClass = classifyContextContent(relativePath);
26162
- if (contentClass === "repo-infra") return null;
26163
- const entry = lstatSync(absolutePath);
26164
- if (entry.isSymbolicLink()) return null;
26165
- const kind = entry.isDirectory() ? "directory" : entry.isFile() && name.endsWith(".md") && name !== "NODE.md" ? "file" : null;
26166
- if (kind === null) return null;
26167
- const document = readNodeDocument(kind === "directory" ? join(absolutePath, "NODE.md") : absolutePath);
26168
- if (document === null) throw new Error(`Context Tree child has invalid or missing metadata: ${relativePath}`);
26169
- return {
26170
- contentClass,
26171
- ...document.description === void 0 ? {} : { description: document.description },
26172
- kind,
26173
- path: relativePath,
26174
- title: document.title
26175
- };
26407
+ function millisecondsSinceModification(path) {
26408
+ try {
26409
+ return Date.now() - statSync(path).mtimeMs;
26410
+ } catch {
26411
+ return;
26412
+ }
26176
26413
  }
26177
- function readTree(treePath, path) {
26178
- const root = resolveTreeRoot(treePath);
26179
- const target = canonicalTarget(root, path);
26180
- const node = readNode(target.absolutePath, target.relativePath, target.kind);
26181
- return {
26182
- children: target.kind === "file" ? [] : readdirSync(target.absolutePath).map((name) => childSummary(root, target.absolutePath, name)).filter((child) => child !== null).sort((left, right) => left.path.localeCompare(right.path)),
26183
- node,
26184
- root,
26185
- schemaVersion: 1,
26186
- target: target.relativePath || "."
26187
- };
26414
+ /**
26415
+ * A preparation is abandoned only when it carries no commit the connected
26416
+ * checkout lacks, has no pending edits, and has gone untouched. Every unknown
26417
+ * answer preserves the worktree, so a `WRITE_OUTDATED` commit awaiting its
26418
+ * retry and a concurrent preparation both survive.
26419
+ */
26420
+ function isAbandonedWrite(root, branch, checkoutBranch, path, runner) {
26421
+ if (optionalGit(root, [
26422
+ "rev-list",
26423
+ "--count",
26424
+ branch,
26425
+ "--not",
26426
+ checkoutBranch
26427
+ ], runner) !== "0") return false;
26428
+ if (path === void 0) return true;
26429
+ const age = millisecondsSinceModification(path);
26430
+ if (age === void 0 || age < ABANDONED_WRITE_AGE_MS) return false;
26431
+ return optionalGit(path, [
26432
+ "status",
26433
+ "--porcelain",
26434
+ "--untracked-files=all"
26435
+ ], runner) === "";
26436
+ }
26437
+ /** Reclaim earlier preparations that were never finished. Every step is best effort. */
26438
+ function reclaimAbandonedWrites(root, checkoutBranch, runner) {
26439
+ optionalGit(root, ["worktree", "prune"], runner);
26440
+ const paths = listWriteWorktrees(root, runner);
26441
+ const branches = optionalGit(root, [
26442
+ "for-each-ref",
26443
+ "--format=%(refname:short)",
26444
+ `refs/heads/${TASK_BRANCH_PREFIX}`
26445
+ ], runner);
26446
+ if (branches === void 0) return;
26447
+ for (const branch of branches.split("\n").filter((value) => value.length > 0)) {
26448
+ const path = paths.get(branch);
26449
+ if (!isAbandonedWrite(root, branch, checkoutBranch, path, runner)) continue;
26450
+ if (path !== void 0) optionalGit(root, [
26451
+ "worktree",
26452
+ "remove",
26453
+ path
26454
+ ], runner);
26455
+ optionalGit(root, [
26456
+ "branch",
26457
+ "-D",
26458
+ branch
26459
+ ], runner);
26460
+ }
26188
26461
  }
26189
26462
  //#endregion
26190
26463
  //#region src/cli/api.ts
@@ -26254,8 +26527,11 @@ function createContextTreeCli(io = defaultIo) {
26254
26527
  line(io, JSON.stringify(result));
26255
26528
  if (!result.ok) process.exitCode = 1;
26256
26529
  });
26257
- program.command("policy").description("Print the canonical packaged Context Tree policy.").action(() => {
26258
- line(io, JSON.stringify(readContextTreePolicy()));
26530
+ program.command("install").description("Install the packaged Context Tree skills into each agent's skill directory.").option("--host <host>", "restrict to one host: claude, codex, or all", "all").option("--project <path>", "install below this project root instead of the home directory").action((options) => {
26531
+ const request = {};
26532
+ if (options.host !== "all") request.hosts = [skillHostSchema.parse(options.host)];
26533
+ if (options.project !== void 0) request.projectPath = resolve(io.cwd(), options.project);
26534
+ line(io, JSON.stringify(installSkills(request)));
26259
26535
  });
26260
26536
  return program;
26261
26537
  }