@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.
- package/README.md +35 -38
- package/dist/cli/index.mjs +923 -647
- package/package.json +7 -28
- package/scripts/postinstall.mjs +53 -0
- package/skills/context-tree-connect/SKILL.md +9 -7
- package/skills/context-tree-create/SKILL.md +10 -10
- package/skills/context-tree-publish/SKILL.md +4 -8
- package/skills/context-tree-read/SKILL.md +32 -12
- package/skills/context-tree-setup/SKILL.md +7 -12
- package/skills/context-tree-write/SKILL.md +196 -17
- package/.agents/plugins/marketplace.json +0 -19
- package/.claude-plugin/marketplace.json +0 -21
- package/.claude-plugin/plugin.json +0 -16
- package/.codex-plugin/plugin.json +0 -36
- package/dist/cli/index.d.mts +0 -1
- package/dist/index.d.mts +0 -77
- package/dist/index.mjs +0 -1449
- package/dist/schemas-C_7izpsa.d.mts +0 -390
- package/dist/schemas-DKHE1sWt.mjs +0 -289
- package/dist/schemas.d.mts +0 -2
- package/dist/schemas.mjs +0 -2
- package/docs/specification.md +0 -160
- package/examples/basic/NODE.md +0 -15
- package/examples/basic/members/NODE.md +0 -8
- package/examples/basic/members/example-agent/NODE.md +0 -7
- package/examples/basic/members/example-agent/memory.md +0 -9
- package/examples/basic/systems/NODE.md +0 -10
- package/examples/basic/systems/runtime.md +0 -15
- package/hooks/hooks.json +0 -26
- package/hooks/session-start.mjs +0 -55
- package/policy/context-tree-policy.md +0 -156
- package/skills/context-tree-connect/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-create/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-publish/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-read/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-setup/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-write/scripts/context-tree.mjs +0 -41
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
-
|
|
13909
|
-
|
|
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
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
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
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
}
|
|
14001
|
-
|
|
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
|
-
|
|
14145
|
-
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
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
|
-
|
|
14176
|
-
function
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
}
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
}
|
|
14187
|
-
|
|
14188
|
-
|
|
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
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
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/
|
|
14238
|
-
function
|
|
14239
|
-
const
|
|
14240
|
-
|
|
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
|
|
14243
|
-
const
|
|
14244
|
-
const
|
|
14245
|
-
|
|
14246
|
-
return
|
|
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
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
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
|
-
|
|
14263
|
-
|
|
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/
|
|
14363
|
+
//#region src/core/internal/root-node.ts
|
|
14267
14364
|
/**
|
|
14268
|
-
*
|
|
14269
|
-
*
|
|
14270
|
-
*
|
|
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
|
-
|
|
14273
|
-
|
|
14274
|
-
|
|
14275
|
-
|
|
14276
|
-
|
|
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
|
-
|
|
14279
|
-
|
|
14280
|
-
|
|
14281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25569
|
-
|
|
25570
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
26112
|
-
|
|
26113
|
-
|
|
26114
|
-
|
|
26115
|
-
|
|
26116
|
-
|
|
26117
|
-
|
|
26118
|
-
|
|
26119
|
-
|
|
26120
|
-
|
|
26121
|
-
|
|
26122
|
-
|
|
26123
|
-
|
|
26124
|
-
|
|
26125
|
-
|
|
26126
|
-
|
|
26127
|
-
|
|
26128
|
-
|
|
26129
|
-
|
|
26130
|
-
|
|
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
|
|
26159
|
-
|
|
26160
|
-
|
|
26161
|
-
|
|
26162
|
-
|
|
26163
|
-
|
|
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
|
-
|
|
26178
|
-
|
|
26179
|
-
|
|
26180
|
-
|
|
26181
|
-
|
|
26182
|
-
|
|
26183
|
-
|
|
26184
|
-
|
|
26185
|
-
|
|
26186
|
-
|
|
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("
|
|
26258
|
-
|
|
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
|
}
|