@visual-z/xeno 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- package/src/vendor/pi-tui/word-navigation.ts +117 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** O.6: explicit evidence relationships are projections, never verification claims. */
|
|
2
|
+
import type { LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
+
|
|
4
|
+
export type EvidenceLink = {
|
|
5
|
+
id: string; work: string; what: string; event: string;
|
|
6
|
+
resource?: string; version?: string;
|
|
7
|
+
freshness: "current" | "stale" | "unknown";
|
|
8
|
+
superseded_by?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Normalize path spelling only; never infer basename, symlink or repository aliases. */
|
|
12
|
+
export function resourceKey(path: string): string {
|
|
13
|
+
const parts: string[] = [];
|
|
14
|
+
for (const part of path.split("/")) {
|
|
15
|
+
if (!part || part === ".") continue;
|
|
16
|
+
if (part === ".." && parts.length > 0 && parts.at(-1) !== "..") parts.pop(); else parts.push(part);
|
|
17
|
+
}
|
|
18
|
+
return (path.startsWith("/") ? "/" : "") + parts.join("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function observationResources(event: ToolResultEvent, input: unknown): string[] {
|
|
22
|
+
if (event.effect && "path" in event.effect) return [resourceKey(event.effect.path)];
|
|
23
|
+
if (!event.ok || input === null || typeof input !== "object") return [];
|
|
24
|
+
const args = input as Record<string, unknown>;
|
|
25
|
+
const paths = typeof args["path"] === "string" ? [args["path"]] : Array.isArray(args["paths"]) ? args["paths"] : [];
|
|
26
|
+
return [...new Set(paths.filter((path): path is string => typeof path === "string").map(resourceKey))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function observedVersion(events: readonly LogEvent[], resource: string): string | null | undefined {
|
|
30
|
+
let version: string | null | undefined;
|
|
31
|
+
for (const event of events) {
|
|
32
|
+
if (event.t === "tool_result" && event.effect && "path" in event.effect && resourceKey(event.effect.path) === resource) {
|
|
33
|
+
version = event.effect.kind === "read" ? event.effect.sha256 : event.effect.sha256_after;
|
|
34
|
+
}
|
|
35
|
+
if (event.t === "snapshot") {
|
|
36
|
+
if (event.removed.map(resourceKey).includes(resource)) version = null;
|
|
37
|
+
for (const [path, value] of Object.entries({ ...event.added, ...event.changed })) if (resourceKey(path) === resource) version = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return version;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function deriveEvidenceLinks(previous: readonly EvidenceLink[], events: readonly LogEvent[]): EvidenceLink[] {
|
|
44
|
+
const links = previous.map(link => ({ ...link }));
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
if (event.t === "evidence_link") {
|
|
47
|
+
if (event.supersedes) for (const link of links) {
|
|
48
|
+
if (link.work === event.work && link.event === event.supersedes && link.superseded_by === undefined) link.superseded_by = event.id;
|
|
49
|
+
}
|
|
50
|
+
if (!links.some(link => link.id === event.id)) links.push({ id: event.id, work: event.work, what: event.what, event: event.source,
|
|
51
|
+
...(event.resource !== undefined ? { resource: resourceKey(event.resource) } : {}),
|
|
52
|
+
...(event.version !== undefined ? { version: event.version } : {}),
|
|
53
|
+
freshness: event.version === undefined || event.observed_version === undefined ? "unknown"
|
|
54
|
+
: event.version === event.observed_version ? "current" : "stale" });
|
|
55
|
+
}
|
|
56
|
+
for (const link of links) {
|
|
57
|
+
if (!link.resource || !link.version) continue;
|
|
58
|
+
if (event.t === "tool_result" && event.effect && "path" in event.effect && resourceKey(event.effect.path) === link.resource) {
|
|
59
|
+
const version = event.effect.kind === "read" ? event.effect.sha256 : event.effect.sha256_after;
|
|
60
|
+
link.freshness = version === link.version ? "current" : "stale";
|
|
61
|
+
}
|
|
62
|
+
if (event.t === "snapshot") {
|
|
63
|
+
if (event.removed.map(resourceKey).includes(link.resource)) link.freshness = "stale";
|
|
64
|
+
for (const [path, version] of Object.entries({ ...event.added, ...event.changed })) {
|
|
65
|
+
if (resourceKey(path) === link.resource) link.freshness = version === link.version ? "current" : "stale";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return links;
|
|
71
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Shared executed-evidence rules for handoff derivation and the turn watchdog. */
|
|
2
|
+
import type { EvidenceQuery, LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
+
import type { Evidence } from "./handoff.ts";
|
|
4
|
+
|
|
5
|
+
type CommandEffect = Extract<NonNullable<ToolResultEvent["effect"]>, { kind: "command" }>;
|
|
6
|
+
export type CommandResult = ToolResultEvent & { effect: CommandEffect };
|
|
7
|
+
export function isCommandResult(event: LogEvent): event is CommandResult {
|
|
8
|
+
return event.t === "tool_result" && event.effect?.kind === "command";
|
|
9
|
+
}
|
|
10
|
+
export function matchesQuery(query: EvidenceQuery, effect: CommandEffect): boolean {
|
|
11
|
+
return "gate" in query ? effect.gate === query.gate : effect.cmd === query.command;
|
|
12
|
+
}
|
|
13
|
+
export function evidenceStale(events: readonly LogEvent[], evidence: Evidence, initially = false): boolean {
|
|
14
|
+
const start = events.findIndex((event) => event.id === evidence.event_id);
|
|
15
|
+
let stale = initially;
|
|
16
|
+
for (const event of events.slice(start + 1)) {
|
|
17
|
+
if (event.t === "tool_result" && event.effect?.kind === "edit") stale = true;
|
|
18
|
+
const sha = event.t === "snapshot" ? event.workspace_sha256
|
|
19
|
+
: event.t === "tool_result" && event.effect?.kind === "command" ? event.effect.workspace_sha256 : undefined;
|
|
20
|
+
if (sha !== undefined && evidence.workspace_sha256 !== undefined) stale = sha !== evidence.workspace_sha256;
|
|
21
|
+
else if (event.t === "snapshot" && (Object.keys(event.added).length + Object.keys(event.changed).length + event.removed.length > 0)) stale = true;
|
|
22
|
+
}
|
|
23
|
+
return stale;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type EvidenceResolution = { ok: true; evidence: Evidence } | {
|
|
27
|
+
ok: false; reason: "no_matching_event" | "nonzero_exit" | "stale_workspace"; exit?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** D11/O.2: callers supply only the current segment's prefix before the claim. */
|
|
31
|
+
export function resolveEvidence(events: readonly LogEvent[], query: EvidenceQuery, afterEvent?: string): EvidenceResolution {
|
|
32
|
+
const start = afterEvent === undefined ? 0 : events.findIndex(event => event.id === afterEvent) + 1;
|
|
33
|
+
const result = events.slice(start).findLast(event => isCommandResult(event) && matchesQuery(query, event.effect));
|
|
34
|
+
if (!result || !isCommandResult(result)) return { ok: false, reason: "no_matching_event" };
|
|
35
|
+
if (result.effect.exit !== 0) return { ok: false, reason: "nonzero_exit", exit: result.effect.exit };
|
|
36
|
+
const evidence: Evidence = { command: result.effect.cmd, exit: result.effect.exit, at: result.at, event_id: result.id,
|
|
37
|
+
...(result.effect.gate !== undefined ? { gate: result.effect.gate } : {}),
|
|
38
|
+
...(result.effect.workspace_sha256 ? { workspace_sha256: result.effect.workspace_sha256 } : {}) };
|
|
39
|
+
return evidenceStale(events, evidence) ? { ok: false, reason: "stale_workspace" } : { ok: true, evidence };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function eligibleProgressChecks(events: readonly LogEvent[], claimed: ReadonlySet<string>, failingGates: { has(gate: string): boolean }): Set<string> {
|
|
43
|
+
const eligible = new Set<string>();
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
if (!isCommandResult(event)) continue;
|
|
46
|
+
const gate = event.effect.gate;
|
|
47
|
+
if (event.effect.exit !== 0 || (gate === undefined && !claimed.has(event.id)) || (gate !== undefined && failingGates.has(gate))) continue;
|
|
48
|
+
if (!evidenceStale(events, { command: event.effect.cmd, exit: 0, at: event.at, event_id: event.id,
|
|
49
|
+
...(event.effect.workspace_sha256 === undefined ? {} : { workspace_sha256: event.effect.workspace_sha256 }) })) eligible.add(event.id);
|
|
50
|
+
}
|
|
51
|
+
return eligible;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An exchange can verify a gate or claim only an already executed command. */
|
|
55
|
+
export function exchangeProgressChecks(events: readonly LogEvent[]): Set<string> {
|
|
56
|
+
const commands: CommandResult[] = [], claimed = new Set<string>(), gates = new Map<string, number>();
|
|
57
|
+
for (const event of events) {
|
|
58
|
+
if (isCommandResult(event)) {
|
|
59
|
+
commands.push(event);
|
|
60
|
+
if (event.effect.gate !== undefined) gates.set(event.effect.gate, event.effect.exit);
|
|
61
|
+
} else if (event.t === "claim") {
|
|
62
|
+
const command = commands.findLast((result) => matchesQuery(event.evidence, result.effect));
|
|
63
|
+
if (command?.effect.exit === 0) claimed.add(command.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return eligibleProgressChecks(events, claimed, new Set([...gates].filter(([, exit]) => exit !== 0).map(([gate]) => gate)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** D11: classify an actual verifier invocation, never a runner word in data. */
|
|
70
|
+
export function classifyGate(gates: Readonly<Record<string, string>>, command: string): string | undefined {
|
|
71
|
+
if (/[\n\r;&|<>`$(){}'"\\]/.test(command)) return undefined;
|
|
72
|
+
const tokens = command.trim().split(/\s+/);
|
|
73
|
+
const informational = ["--version", "-V", "--help", "-h", "--collect-only", "--listTests", "--showConfig", "--init", "--fixtures", "--markers"];
|
|
74
|
+
if (tokens.some((t) => informational.includes(t))) return undefined;
|
|
75
|
+
let i = 0;
|
|
76
|
+
const first = tokens[0];
|
|
77
|
+
if (first === "python" || first === "python3") {
|
|
78
|
+
if (tokens[1] !== "-m") return undefined;
|
|
79
|
+
i = 2;
|
|
80
|
+
} else if (["npx", "bunx", "uvx"].includes(first ?? "")) i = 1;
|
|
81
|
+
else if (["uv", "poetry", "pdm", "pipenv"].includes(first ?? "")) {
|
|
82
|
+
if (tokens[1] !== "run") return undefined;
|
|
83
|
+
i = 2;
|
|
84
|
+
} else if (["bun", "npm", "pnpm", "yarn"].includes(first ?? "") && tokens[1] === "run") i = 2;
|
|
85
|
+
const executable = tokens[i];
|
|
86
|
+
if (executable === "cargo" && !["test", "check", "clippy", "build"].includes(tokens[i + 1] ?? "")) return undefined;
|
|
87
|
+
if (executable === "vitest" && ["list", "init"].includes(tokens[i + 1] ?? "")) return undefined;
|
|
88
|
+
return gates[executable ?? ""];
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** D5/D11: incomplete execution is unknown, never an instruction to run twice. */
|
|
2
|
+
import type { LogEvent, AssistantBlock } from "./events.ts";
|
|
3
|
+
|
|
4
|
+
export function pendingToolCalls(events: readonly LogEvent[]): Extract<AssistantBlock, { type: "tool_use" }>[] {
|
|
5
|
+
const pending = new Map<string, Extract<AssistantBlock, { type: "tool_use" }>>();
|
|
6
|
+
for (const event of events) {
|
|
7
|
+
if (event.t === "assistant") {
|
|
8
|
+
for (const block of event.blocks) if (block.type === "tool_use") pending.set(block.call_id, block);
|
|
9
|
+
} else if (event.t === "tool_result") pending.delete(event.call_id);
|
|
10
|
+
}
|
|
11
|
+
return [...pending.values()];
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exhaustiveness for discriminated unions.
|
|
3
|
+
*
|
|
4
|
+
* `LogEvent` has ten variants and `Effect` three; the projection's claim to be
|
|
5
|
+
* a total function rests on every switch over them covering every case. In the
|
|
6
|
+
* default branch of such a switch the value narrows to `never`, so passing it
|
|
7
|
+
* here compiles only while the switch is complete -- adding a variant turns
|
|
8
|
+
* every incomplete switch into a type error rather than a silent fallthrough.
|
|
9
|
+
*
|
|
10
|
+
* This is the one exhaustiveness guarantee tsc will not give on its own: a
|
|
11
|
+
* switch that misses a case is legal TypeScript, it just falls through.
|
|
12
|
+
*/
|
|
13
|
+
export function assertNever(value: never, context: string): never {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`${context}: unhandled variant ${JSON.stringify(value)} -- ` +
|
|
16
|
+
"a union gained a member without every switch over it being updated",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Experimental projections never mutate the durable event stream (D8, L). */
|
|
2
|
+
import type { Message } from "./project.ts";
|
|
3
|
+
|
|
4
|
+
export type ContextArm = "node" | "replay" | "truncate" | "mask";
|
|
5
|
+
|
|
6
|
+
export function experimentalContext(messages: readonly Message[], arm: ContextArm, keepRounds: number): Message[] {
|
|
7
|
+
if (!Number.isSafeInteger(keepRounds) || keepRounds < 1) throw new RangeError("keepRounds must be positive");
|
|
8
|
+
if (arm === "node" || arm === "replay") return [...messages];
|
|
9
|
+
const groups: Message[][] = [[]];
|
|
10
|
+
for (const message of messages) {
|
|
11
|
+
if (message.role === "assistant") groups.push([]);
|
|
12
|
+
groups.at(-1)!.push(message);
|
|
13
|
+
}
|
|
14
|
+
const recent = Math.max(1, groups.length - keepRounds);
|
|
15
|
+
if (arm === "truncate") return [...groups[0]!, ...groups.slice(recent).flat()];
|
|
16
|
+
return groups.flatMap((group, i) => i >= recent ? group : group.map((message) => ({
|
|
17
|
+
...message,
|
|
18
|
+
content: message.content.map((block) => block.type === "tool_result"
|
|
19
|
+
? { ...block, content: "[observation masked by experiment; original remains in log]" } : block),
|
|
20
|
+
})));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Fisher-Yates with a fixed seed; scheduling and intervals are reproducible. */
|
|
24
|
+
export function seededRandom(seed: number): () => number {
|
|
25
|
+
let state = seed >>> 0;
|
|
26
|
+
return () => {
|
|
27
|
+
state = (Math.imul(1664525, state) + 1013904223) >>> 0;
|
|
28
|
+
return state / 4294967296;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shuffled<T>(items: readonly T[], seed: number): T[] {
|
|
33
|
+
const random = seededRandom(seed);
|
|
34
|
+
const out = [...items];
|
|
35
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
36
|
+
const j = Math.floor(random() * (i + 1));
|
|
37
|
+
[out[i], out[j]] = [out[j]!, out[i]!];
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript serialisation. Pure: no I/O, no clock.
|
|
3
|
+
*
|
|
4
|
+
* The HTML has to be a single file a person can open offline. A stylesheet
|
|
5
|
+
* or font loaded from a CDN is blank in the one situation export is for, so
|
|
6
|
+
* nothing here emits a URL. Timestamps are passed in so this module cannot
|
|
7
|
+
* invent "now".
|
|
8
|
+
*
|
|
9
|
+
* User text is escaped as HTML text. A transcript that contains
|
|
10
|
+
* `<script>alert(1)</script>` must stay text; emitting it as a tag is XSS
|
|
11
|
+
* from the log (card: feat-session-flags-and-export).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AssistantBlock, LogEvent } from "./events.ts";
|
|
15
|
+
import { assertNever } from "./exhaustive.ts";
|
|
16
|
+
|
|
17
|
+
export interface HtmlMeta {
|
|
18
|
+
title: string;
|
|
19
|
+
/** ISO timestamp supplied by the caller. Never read from the clock here. */
|
|
20
|
+
exportedAt: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function toJsonl(events: readonly LogEvent[]): string {
|
|
25
|
+
if (events.length === 0) return "";
|
|
26
|
+
return events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toHtml(events: readonly LogEvent[], meta: HtmlMeta): string {
|
|
30
|
+
const title = escapeHtml(meta.title);
|
|
31
|
+
const when = escapeHtml(meta.exportedAt);
|
|
32
|
+
const id = meta.sessionId !== undefined ? escapeHtml(meta.sessionId) : "";
|
|
33
|
+
const body = events.map(eventHtml).join("\n");
|
|
34
|
+
return `<!DOCTYPE html>
|
|
35
|
+
<html lang="en">
|
|
36
|
+
<head>
|
|
37
|
+
<meta charset="utf-8">
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39
|
+
<title>${title}</title>
|
|
40
|
+
<style>
|
|
41
|
+
body{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;max-width:72rem;margin:1.5rem auto;padding:0 1rem;color:#111;background:#fff;line-height:1.45}
|
|
42
|
+
header{border-bottom:1px solid #ddd;padding-bottom:0.75rem;margin-bottom:1rem}
|
|
43
|
+
header p{color:#555;margin:0.25rem 0 0}
|
|
44
|
+
.event{margin:0.85rem 0;padding:0.55rem 0.8rem;border-left:3px solid #ccc;background:#fafafa}
|
|
45
|
+
.user{border-color:#1565c0}
|
|
46
|
+
.assistant{border-color:#2e7d32}
|
|
47
|
+
.tool{border-color:#ef6c00}
|
|
48
|
+
.meta{color:#666;font-size:0.8rem;margin-bottom:0.35rem}
|
|
49
|
+
pre{white-space:pre-wrap;word-break:break-word;margin:0;font:inherit}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<header>
|
|
54
|
+
<h1>${title}</h1>
|
|
55
|
+
<p>exported ${when}${id.length > 0 ? ` · ${id}` : ""}</p>
|
|
56
|
+
</header>
|
|
57
|
+
<main>
|
|
58
|
+
${body}
|
|
59
|
+
</main>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Most recent assistant text block. `/copy` is this string, not a
|
|
67
|
+
* reconstruction of thinking or tool JSON.
|
|
68
|
+
*/
|
|
69
|
+
export function lastAssistantText(events: readonly LogEvent[]): string | null {
|
|
70
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
71
|
+
const e = events[i]!;
|
|
72
|
+
if (e.t !== "assistant") continue;
|
|
73
|
+
const parts: string[] = [];
|
|
74
|
+
for (const b of e.blocks) {
|
|
75
|
+
if (b.type === "text") parts.push(b.text);
|
|
76
|
+
}
|
|
77
|
+
if (parts.length > 0) return parts.join("");
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function escapeHtml(s: string): string {
|
|
83
|
+
return s
|
|
84
|
+
.replace(/&/g, "&")
|
|
85
|
+
.replace(/</g, "<")
|
|
86
|
+
.replace(/>/g, ">")
|
|
87
|
+
.replace(/"/g, """)
|
|
88
|
+
.replace(/'/g, "'");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function eventHtml(e: LogEvent): string {
|
|
92
|
+
switch (e.t) {
|
|
93
|
+
case "user":
|
|
94
|
+
return wrap("user", "user", e.at, e.text);
|
|
95
|
+
case "guidance":
|
|
96
|
+
return wrap("event", `harness ${e.reason}`, e.at, e.text);
|
|
97
|
+
case "assistant":
|
|
98
|
+
return wrap("assistant", "assistant", e.at, assistantText(e.blocks));
|
|
99
|
+
case "interrupt":
|
|
100
|
+
return wrap("event", "interrupt", e.at, `cancelled ${e.user_id}`);
|
|
101
|
+
case "tool_result":
|
|
102
|
+
return wrap("tool", `tool_result ${e.ok ? "ok" : "fail"}`, e.at, e.content);
|
|
103
|
+
case "cut":
|
|
104
|
+
return wrap("event", "cut", e.at, `${e.reason} → ${e.handoff_ref}`);
|
|
105
|
+
case "snapshot":
|
|
106
|
+
return wrap(
|
|
107
|
+
"event",
|
|
108
|
+
"snapshot",
|
|
109
|
+
e.at,
|
|
110
|
+
[
|
|
111
|
+
...Object.keys(e.added).map((p) => `+ ${p}`),
|
|
112
|
+
...Object.keys(e.changed).map((p) => `~ ${p}`),
|
|
113
|
+
...e.removed.map((p) => `- ${p}`),
|
|
114
|
+
].join("\n") || "(no file changes)",
|
|
115
|
+
);
|
|
116
|
+
case "rewind":
|
|
117
|
+
return wrap("event", `rewind ${e.mode}`, e.at, e.prompt);
|
|
118
|
+
case "spawn":
|
|
119
|
+
return wrap("event", "spawn", e.at, e.purpose);
|
|
120
|
+
case "joined":
|
|
121
|
+
return wrap("event", "joined", e.at, e.summary);
|
|
122
|
+
case "adopt":
|
|
123
|
+
return wrap("event", "adopt", e.at, `branch ${e.branch.node}`);
|
|
124
|
+
case "discard":
|
|
125
|
+
return wrap("event", "discard", e.at, e.reason);
|
|
126
|
+
case "claim":
|
|
127
|
+
return wrap("event", "claim", e.at, e.what);
|
|
128
|
+
case "decision":
|
|
129
|
+
return wrap("event", "decision", e.at, `${e.chose} — ${e.because}`);
|
|
130
|
+
case "pitfall":
|
|
131
|
+
return wrap("event", "pitfall", e.at, `${e.tried} — ${e.failed_because}`);
|
|
132
|
+
case "blocked":
|
|
133
|
+
return wrap("event", "blocked", e.at, `${e.what} — ${e.reason}`);
|
|
134
|
+
case "question":
|
|
135
|
+
return wrap("event", "question", e.at, e.text);
|
|
136
|
+
case "next_action":
|
|
137
|
+
return wrap("event", "next_action", e.at, `${e.what} → ${e.next_action}`);
|
|
138
|
+
case "work_status":
|
|
139
|
+
return wrap("event", `work ${e.operation}`, e.at, `${e.work_id}: ${e.what}`);
|
|
140
|
+
case "evidence_link":
|
|
141
|
+
return wrap("event", "evidence link", e.at, `${e.work}: ${e.what} → ${e.source}`);
|
|
142
|
+
default:
|
|
143
|
+
return assertNever(e, "eventHtml");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assistantText(blocks: readonly AssistantBlock[]): string {
|
|
148
|
+
const parts: string[] = [];
|
|
149
|
+
for (const b of blocks) {
|
|
150
|
+
switch (b.type) {
|
|
151
|
+
case "text":
|
|
152
|
+
parts.push(b.text);
|
|
153
|
+
break;
|
|
154
|
+
case "thinking":
|
|
155
|
+
parts.push(b.text);
|
|
156
|
+
break;
|
|
157
|
+
case "tool_use":
|
|
158
|
+
parts.push(`${b.name} ${JSON.stringify(b.input)}`);
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
return assertNever(b, "assistantText");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return parts.join("\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function wrap(cls: string, kind: string, at: string, text: string): string {
|
|
168
|
+
return `<article class="event ${cls}"><div class="meta">${escapeHtml(kind)} · ${escapeHtml(at)}</div><pre>${escapeHtml(text)}</pre></article>`;
|
|
169
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Explicit whitespace-delimited @ references; code literals and emails remain text. */
|
|
2
|
+
export interface FileReference { readonly start: number; readonly end: number; readonly path: string }
|
|
3
|
+
|
|
4
|
+
export function fileReferences(text: string): FileReference[] {
|
|
5
|
+
const refs: FileReference[] = [];
|
|
6
|
+
let codeTicks = 0;
|
|
7
|
+
for (let i = 0; i < text.length; i++) {
|
|
8
|
+
if (text[i] === "\\") { i++; continue; }
|
|
9
|
+
if (text[i] === "`") {
|
|
10
|
+
let end = i;
|
|
11
|
+
while (text[end] === "`") end++;
|
|
12
|
+
const count = end - i;
|
|
13
|
+
if (codeTicks === 0) codeTicks = count;
|
|
14
|
+
else if (count === codeTicks) codeTicks = 0;
|
|
15
|
+
i = end - 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (codeTicks > 0 || text[i] !== "@" || (i > 0 && !/\s/.test(text[i - 1]!))) continue;
|
|
19
|
+
const start = i;
|
|
20
|
+
let end = i + 1;
|
|
21
|
+
let path: string;
|
|
22
|
+
if (text[end] === '"') {
|
|
23
|
+
const close = text.indexOf('"', end + 1);
|
|
24
|
+
if (close < 0 || text.slice(end, close).includes("\n")) continue;
|
|
25
|
+
path = text.slice(end + 1, close);
|
|
26
|
+
end = close + 1;
|
|
27
|
+
} else {
|
|
28
|
+
while (end < text.length && !/\s/.test(text[end]!)) end++;
|
|
29
|
+
path = text.slice(start + 1, end);
|
|
30
|
+
}
|
|
31
|
+
if (path.length > 0) refs.push({ start, end, path });
|
|
32
|
+
i = end - 1;
|
|
33
|
+
}
|
|
34
|
+
return refs;
|
|
35
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restricted YAML frontmatter reader.
|
|
3
|
+
*
|
|
4
|
+
* Why hand-written instead of a YAML package: the emitter in yaml.ts already
|
|
5
|
+
* made that call for writes (AGENTS.md cites it as the example). This is the
|
|
6
|
+
* read side of the same choice. A full parser would pull in aliases, anchors
|
|
7
|
+
* and tags on a path that reads user and project files.
|
|
8
|
+
*
|
|
9
|
+
* Supported: `key: value`, quoted strings, booleans, and `[a, b]` arrays.
|
|
10
|
+
* Nested mappings and multiline blocks are named and skipped; siblings stay.
|
|
11
|
+
* The function does not throw — a broken header must not take down startup.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type FrontmatterValue = string | boolean | readonly string[];
|
|
15
|
+
|
|
16
|
+
export interface FrontmatterParse {
|
|
17
|
+
readonly fields: { readonly [key: string]: FrontmatterValue };
|
|
18
|
+
readonly body: string;
|
|
19
|
+
readonly diagnostics: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const KEY = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
23
|
+
const BOOL = /^(?:true|false)$/i;
|
|
24
|
+
|
|
25
|
+
export function parseFrontmatter(text: string): FrontmatterParse {
|
|
26
|
+
try {
|
|
27
|
+
return parseInner(text);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
return { fields: {}, body: text, diagnostics: [`frontmatter: ${message}`] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseInner(text: string): FrontmatterParse {
|
|
35
|
+
const normalized = stripBom(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
36
|
+
const extracted = extract(normalized);
|
|
37
|
+
if (extracted.yaml === null) {
|
|
38
|
+
return { fields: {}, body: extracted.body, diagnostics: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fields: { [key: string]: FrontmatterValue } = {};
|
|
42
|
+
const diagnostics: string[] = [];
|
|
43
|
+
const lines = extracted.yaml.split("\n");
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i]!;
|
|
47
|
+
if (line.trim().length === 0 || line.trimStart().startsWith("#")) continue;
|
|
48
|
+
|
|
49
|
+
if (/^\s/.test(line)) {
|
|
50
|
+
diagnostics.push(`frontmatter: indented line is not a supported field`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const match = KEY.exec(line);
|
|
55
|
+
if (!match) {
|
|
56
|
+
diagnostics.push(`frontmatter: unparseable line ${JSON.stringify(line)}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const key = match[1]!;
|
|
61
|
+
const raw = match[2] ?? "";
|
|
62
|
+
|
|
63
|
+
if (isNestedMarker(raw)) {
|
|
64
|
+
diagnostics.push(`frontmatter: nested mapping "${key}" is not supported`);
|
|
65
|
+
i = skipIndented(lines, i);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parsed = parseValue(raw);
|
|
70
|
+
if (parsed === undefined) {
|
|
71
|
+
diagnostics.push(`frontmatter: nested mapping "${key}" is not supported`);
|
|
72
|
+
i = skipIndented(lines, i);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
fields[key] = parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { fields, body: extracted.body, diagnostics };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extract(text: string): { yaml: string | null; body: string } {
|
|
82
|
+
if (!text.startsWith("---")) return { yaml: null, body: text };
|
|
83
|
+
const lines = text.split("\n");
|
|
84
|
+
if (lines[0] !== "---") return { yaml: null, body: text };
|
|
85
|
+
let end = -1;
|
|
86
|
+
for (let i = 1; i < lines.length; i++) {
|
|
87
|
+
if (lines[i] === "---") {
|
|
88
|
+
end = i;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (end < 0) return { yaml: null, body: text };
|
|
93
|
+
return {
|
|
94
|
+
yaml: lines.slice(1, end).join("\n"),
|
|
95
|
+
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "").replace(/\s+$/, ""),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isNestedMarker(raw: string): boolean {
|
|
100
|
+
const value = raw.trim();
|
|
101
|
+
return value.length === 0 || value === "|" || value === "|-" || value === "|+" ||
|
|
102
|
+
value === ">" || value === ">-" || value === ">+";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseValue(raw: string): FrontmatterValue | undefined {
|
|
106
|
+
const trimmed = raw.trim();
|
|
107
|
+
if (trimmed.startsWith("{")) return undefined;
|
|
108
|
+
if (trimmed.startsWith("[")) return parseFlowArray(trimmed);
|
|
109
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
110
|
+
const quoted = parseQuoted(trimmed);
|
|
111
|
+
return quoted ?? trimmed;
|
|
112
|
+
}
|
|
113
|
+
const unquoted = stripInlineComment(trimmed);
|
|
114
|
+
if (BOOL.test(unquoted)) return unquoted.toLowerCase() === "true";
|
|
115
|
+
return unquoted;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Flow arrays of scalars only. A mapping inside the brackets is a nested
|
|
120
|
+
* structure we refuse, so the caller can name the field and keep going.
|
|
121
|
+
*/
|
|
122
|
+
function parseFlowArray(raw: string): readonly string[] | undefined {
|
|
123
|
+
if (!raw.startsWith("[") || !raw.endsWith("]")) return undefined;
|
|
124
|
+
const inner = raw.slice(1, -1).trim();
|
|
125
|
+
if (inner.length === 0) return [];
|
|
126
|
+
if (inner.includes("{") || inner.includes("[") || /(?:^|,)\s*[A-Za-z_][A-Za-z0-9_-]*\s*:/.test(inner)) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const items: string[] = [];
|
|
130
|
+
let buf = "";
|
|
131
|
+
let quote: string | null = null;
|
|
132
|
+
for (let i = 0; i < inner.length; i++) {
|
|
133
|
+
const ch = inner[i]!;
|
|
134
|
+
if (quote) {
|
|
135
|
+
if (quote === "'" && ch === "'" && inner[i + 1] === "'") {
|
|
136
|
+
buf += "'";
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === quote && (quote === "'" || inner[i - 1] !== "\\")) {
|
|
141
|
+
quote = null;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
buf += ch;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === '"' || ch === "'") {
|
|
148
|
+
quote = ch;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (ch === ",") {
|
|
152
|
+
items.push(buf.trim());
|
|
153
|
+
buf = "";
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
buf += ch;
|
|
157
|
+
}
|
|
158
|
+
items.push(buf.trim());
|
|
159
|
+
return items;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseQuoted(raw: string): string | undefined {
|
|
163
|
+
const q = raw[0];
|
|
164
|
+
if (q !== '"' && q !== "'") return undefined;
|
|
165
|
+
let out = "";
|
|
166
|
+
for (let i = 1; i < raw.length; i++) {
|
|
167
|
+
const ch = raw[i]!;
|
|
168
|
+
if (q === "'" && ch === "'" && raw[i + 1] === "'") {
|
|
169
|
+
out += "'";
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === q) {
|
|
174
|
+
const rest = raw.slice(i + 1).trim();
|
|
175
|
+
if (rest.length > 0 && !rest.startsWith("#")) return undefined;
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
if (q === '"' && ch === "\\" && i + 1 < raw.length) {
|
|
179
|
+
const next = raw[i + 1]!;
|
|
180
|
+
out += next === "n" ? "\n" : next === "t" ? "\t" : next;
|
|
181
|
+
i++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
out += ch;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stripInlineComment(value: string): string {
|
|
190
|
+
const idx = value.indexOf(" #");
|
|
191
|
+
return (idx >= 0 ? value.slice(0, idx) : value).trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function skipIndented(lines: readonly string[], i: number): number {
|
|
195
|
+
let j = i + 1;
|
|
196
|
+
while (j < lines.length) {
|
|
197
|
+
const next = lines[j]!;
|
|
198
|
+
if (next.trim().length === 0) {
|
|
199
|
+
j++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (/^\s/.test(next)) {
|
|
203
|
+
j++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
return j - 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stripBom(text: string): string {
|
|
212
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
213
|
+
}
|