@zenera/cli 1.1.24 → 1.1.25
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 +53 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +80 -7
- package/dist/commands/inspect.d.ts +47 -1
- package/dist/commands/inspect.js +167 -18
- package/dist/commands/memory.js +128 -29
- package/dist/commands/meta.js +311 -21
- package/dist/commands/models.js +96 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +76 -14
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +1 -0
- package/dist/eventlog.d.ts +59 -0
- package/dist/eventlog.js +146 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +15 -1
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +60 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +338 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +125 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +45 -0
- package/dist/meta/finetune/feedback.js +85 -0
- package/dist/meta/finetune/improvements.d.ts +51 -0
- package/dist/meta/finetune/improvements.js +244 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +74 -0
- package/dist/meta/finetune/live.js +226 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +114 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/report.d.ts +33 -0
- package/dist/meta/finetune/report.js +1284 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +41 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +39 -0
- package/dist/meta/finetune/train.js +391 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +291 -0
- package/dist/meta/finetune/tuning.d.ts +167 -0
- package/dist/meta/finetune/tuning.js +289 -0
- package/dist/meta/finetune/usage.d.ts +62 -0
- package/dist/meta/finetune/usage.js +225 -0
- package/dist/meta/host.d.ts +14 -0
- package/dist/meta/host.js +20 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -6
- package/dist/scaffold.js +5 -60
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.js +53 -2
- package/dist/tui/models.d.ts +33 -0
- package/dist/tui/models.js +389 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +31 -102
- package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +47 -9
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1449 -658
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +60 -25
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +18 -14
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +7 -3
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/memory-instructions.md +31 -32
- package/templates/project/agents/tools-instructions.md +7 -4
- package/templates/project/gitignore +2 -0
- package/dist/meta.d.ts +0 -221
- package/dist/meta.js +0 -684
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type KeyStore, type Provider } from './host.ts';
|
|
2
|
+
/** The variable a project's `.env` names it with — a zen ref, not a wire id. */
|
|
3
|
+
export declare const MODEL_ENV = "ZENERA_META_MODEL";
|
|
4
|
+
export type ModelSource = 'flag' | 'shell' | 'env' | 'store' | 'project' | 'default';
|
|
5
|
+
export interface ModelChoice {
|
|
6
|
+
/** a zen ref: `vertex/gemini-3.8-flash`, or a bare id for the default provider */
|
|
7
|
+
ref: string;
|
|
8
|
+
from: ModelSource;
|
|
9
|
+
}
|
|
10
|
+
export interface ModelSources {
|
|
11
|
+
flag?: string;
|
|
12
|
+
/** `ZENERA_META_MODEL` as it stood before the project's `.env` was folded in */
|
|
13
|
+
shell?: string;
|
|
14
|
+
/** the same variable after it was, which is the `.env` answer when they differ */
|
|
15
|
+
env?: string;
|
|
16
|
+
stored?: string;
|
|
17
|
+
/** the project's `agents.yaml` `model:` */
|
|
18
|
+
project?: string;
|
|
19
|
+
/** what a held key recommends, when nothing above answered */
|
|
20
|
+
fallback?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Where the answer is looked for, best first. */
|
|
23
|
+
export declare const ORDER: readonly ModelSource[];
|
|
24
|
+
export declare function chooseModel(sources: ModelSources): ModelChoice | undefined;
|
|
25
|
+
export declare const SOURCE_LABELS: Record<ModelSource, string>;
|
|
26
|
+
/** First of each row is the default; the rest are what `--pick` offers beside it. */
|
|
27
|
+
export declare const RECOMMENDED: Partial<Record<Provider, readonly string[]>>;
|
|
28
|
+
/** The recommended ref for the first provider with a key, or for `only`. */
|
|
29
|
+
export declare function defaultRef(store: KeyStore, only?: Provider): string | undefined;
|
|
30
|
+
export interface MetaFile {
|
|
31
|
+
version: 1;
|
|
32
|
+
model?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const readMeta: () => Promise<MetaFile>;
|
|
35
|
+
export declare function writeMeta(file: MetaFile): void;
|
|
36
|
+
/**
|
|
37
|
+
* The provider a misspelt prefix was reaching for, if that is what it is.
|
|
38
|
+
*
|
|
39
|
+
* `vertes/gemini-3.8-flash` is shaped exactly like the OpenRouter id
|
|
40
|
+
* `meta-llama/llama-4`, so an unknown prefix cannot be an error on its own —
|
|
41
|
+
* only one close enough to a real provider name to be a slip of the hand.
|
|
42
|
+
*/
|
|
43
|
+
export declare function misspelledProvider(ref: string): Provider | undefined;
|
|
44
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { paths, PROVIDER_NAMES, readJson, writeJson, } from "./host.js";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// The model
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/** The variable a project's `.env` names it with — a zen ref, not a wire id. */
|
|
6
|
+
export const MODEL_ENV = 'ZENERA_META_MODEL';
|
|
7
|
+
/** Where the answer is looked for, best first. */
|
|
8
|
+
export const ORDER = [
|
|
9
|
+
'flag',
|
|
10
|
+
'shell',
|
|
11
|
+
'env',
|
|
12
|
+
'store',
|
|
13
|
+
'project',
|
|
14
|
+
'default',
|
|
15
|
+
];
|
|
16
|
+
export function chooseModel(sources) {
|
|
17
|
+
const at = {
|
|
18
|
+
flag: sources.flag,
|
|
19
|
+
// `.env` only fills gaps, so an answer that survived it came from the shell
|
|
20
|
+
shell: sources.shell,
|
|
21
|
+
env: sources.shell ? undefined : sources.env,
|
|
22
|
+
store: sources.stored,
|
|
23
|
+
project: sources.project,
|
|
24
|
+
default: sources.fallback,
|
|
25
|
+
};
|
|
26
|
+
for (const from of ORDER) {
|
|
27
|
+
const ref = at[from]?.trim();
|
|
28
|
+
if (ref) {
|
|
29
|
+
return { ref, from };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export const SOURCE_LABELS = {
|
|
35
|
+
flag: '--model',
|
|
36
|
+
shell: `${MODEL_ENV} (shell)`,
|
|
37
|
+
env: `${MODEL_ENV} (.env)`,
|
|
38
|
+
store: 'zen meta model',
|
|
39
|
+
project: 'agents.yaml model:',
|
|
40
|
+
default: 'recommended',
|
|
41
|
+
};
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// What to run when nobody said
|
|
44
|
+
//
|
|
45
|
+
// A first `zen meta` should work on a machine that has a key and nothing else,
|
|
46
|
+
// so the last resort is the best model the keys on hand can buy rather than an
|
|
47
|
+
// error. Deep tier deliberately: this agent is pointed at the project itself,
|
|
48
|
+
// and a cheap model reading a specification is a false economy.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/** First of each row is the default; the rest are what `--pick` offers beside it. */
|
|
51
|
+
export const RECOMMENDED = {
|
|
52
|
+
openai: ['gpt-5.6-sol', 'gpt-5.6-terra'],
|
|
53
|
+
anthropic: ['claude-opus-5', 'claude-sonnet-5'],
|
|
54
|
+
vertex: ['gemini-3.8-flash', 'gemini-3.5-flash-lite'],
|
|
55
|
+
google: ['gemini-3.8-flash', 'gemini-3.5-flash-lite'],
|
|
56
|
+
openrouter: ['anthropic/claude-opus-5', 'openai/gpt-5.6-sol'],
|
|
57
|
+
};
|
|
58
|
+
/** Which provider is tried first when several are held. */
|
|
59
|
+
const PREFERENCE = ['anthropic', 'openai', 'vertex', 'google', 'openrouter'];
|
|
60
|
+
/** The recommended ref for the first provider with a key, or for `only`. */
|
|
61
|
+
export function defaultRef(store, only) {
|
|
62
|
+
for (const provider of only ? [only] : PREFERENCE) {
|
|
63
|
+
const id = RECOMMENDED[provider]?.[0];
|
|
64
|
+
if (id && store.active(provider)) {
|
|
65
|
+
return `${provider}/${id}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const EMPTY = { version: 1 };
|
|
71
|
+
export const readMeta = () => readJson(paths.meta(), EMPTY);
|
|
72
|
+
export function writeMeta(file) {
|
|
73
|
+
writeJson(paths.meta(), file);
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Refs
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* The provider a misspelt prefix was reaching for, if that is what it is.
|
|
80
|
+
*
|
|
81
|
+
* `vertes/gemini-3.8-flash` is shaped exactly like the OpenRouter id
|
|
82
|
+
* `meta-llama/llama-4`, so an unknown prefix cannot be an error on its own —
|
|
83
|
+
* only one close enough to a real provider name to be a slip of the hand.
|
|
84
|
+
*/
|
|
85
|
+
export function misspelledProvider(ref) {
|
|
86
|
+
const slash = ref.indexOf('/');
|
|
87
|
+
if (slash <= 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const head = ref.slice(0, slash).toLowerCase();
|
|
91
|
+
if (PROVIDER_NAMES.has(head)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
for (const name of PROVIDER_NAMES) {
|
|
95
|
+
// Two, so that a transposition — which Levenshtein counts twice — reads
|
|
96
|
+
// as the typo it is. No provider name is short enough for that to be loose.
|
|
97
|
+
if (distance(head, name) <= 2) {
|
|
98
|
+
return name;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Levenshtein, one row at a time. Both operands are a word long. */
|
|
104
|
+
function distance(a, b) {
|
|
105
|
+
if (Math.abs(a.length - b.length) > 2) {
|
|
106
|
+
return 3;
|
|
107
|
+
}
|
|
108
|
+
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
109
|
+
for (let i = 1; i <= a.length; i++) {
|
|
110
|
+
const next = [i];
|
|
111
|
+
for (let j = 1; j <= b.length; j++) {
|
|
112
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
113
|
+
next[j] = Math.min(next[j - 1] + 1, row[j] + 1, row[j - 1] + cost);
|
|
114
|
+
}
|
|
115
|
+
row = next;
|
|
116
|
+
}
|
|
117
|
+
return row[b.length];
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const PROMPT_DIR = ".github/prompts";
|
|
2
|
+
/** `/project-review`, `project-review`, `project-review.prompt.md` or a path. */
|
|
3
|
+
export declare function promptPath(dir: string, name: string): string;
|
|
4
|
+
export interface StoredPrompt {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Frontmatter is the editor's business — `mode`, `tools`, `description` — and
|
|
12
|
+
* none of it means anything to copilot, so only the body is sent. `description`
|
|
13
|
+
* is kept for the one line of narration that says which prompt is running.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readPrompt(path: string, name: string, text: string): StoredPrompt;
|
|
16
|
+
export declare function loadPrompt(dir: string, name: string): Promise<StoredPrompt>;
|
|
17
|
+
export declare function listPrompts(dir: string): Promise<string[]>;
|
|
18
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { usageError } from "./host.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Stored prompts
|
|
6
|
+
//
|
|
7
|
+
// Copilot reads `AGENTS.md`, `.github/skills/` and `.github/agents/`, but it
|
|
8
|
+
// has no notion of `.github/prompts/*.prompt.md` — the files an editor offers
|
|
9
|
+
// as slash commands. Reading one and handing over its body is the whole of
|
|
10
|
+
// `zen meta run`, and it is why that subcommand exists.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const PROMPT_DIR = '.github/prompts';
|
|
13
|
+
const PROMPT_SUFFIX = '.prompt.md';
|
|
14
|
+
/** `/project-review`, `project-review`, `project-review.prompt.md` or a path. */
|
|
15
|
+
export function promptPath(dir, name) {
|
|
16
|
+
const bare = name.replace(/^\//, '');
|
|
17
|
+
if (bare.includes('/') || bare.endsWith('.md')) {
|
|
18
|
+
return `${dir}/${bare}`;
|
|
19
|
+
}
|
|
20
|
+
return `${dir}/${PROMPT_DIR}/${bare}${PROMPT_SUFFIX}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Frontmatter is the editor's business — `mode`, `tools`, `description` — and
|
|
24
|
+
* none of it means anything to copilot, so only the body is sent. `description`
|
|
25
|
+
* is kept for the one line of narration that says which prompt is running.
|
|
26
|
+
*/
|
|
27
|
+
export function readPrompt(path, name, text) {
|
|
28
|
+
let body = text;
|
|
29
|
+
let description;
|
|
30
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
31
|
+
if (match) {
|
|
32
|
+
body = text.slice(match[0].length);
|
|
33
|
+
const found = /^description:\s*(.+)$/m.exec(match[1]);
|
|
34
|
+
description = found?.[1].trim().replace(/^['"]|['"]$/g, '');
|
|
35
|
+
}
|
|
36
|
+
return { name, path, description, body: body.trim() };
|
|
37
|
+
}
|
|
38
|
+
export async function loadPrompt(dir, name) {
|
|
39
|
+
const path = promptPath(dir, name);
|
|
40
|
+
if (!existsSync(path)) {
|
|
41
|
+
const known = await listPrompts(dir);
|
|
42
|
+
throw usageError(`no prompt named ${name.replace(/^\//, '')}`, known.length > 0
|
|
43
|
+
? `try: ${known.map((p) => `/${p}`).join(', ')}`
|
|
44
|
+
: `put one in ${PROMPT_DIR}/`);
|
|
45
|
+
}
|
|
46
|
+
return readPrompt(path, name.replace(/^\//, ''), await readFile(path, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
export async function listPrompts(dir) {
|
|
49
|
+
const { readdir } = await import('node:fs/promises');
|
|
50
|
+
try {
|
|
51
|
+
const names = await readdir(`${dir}/${PROMPT_DIR}`);
|
|
52
|
+
return names
|
|
53
|
+
.filter((n) => n.endsWith(PROMPT_SUFFIX))
|
|
54
|
+
.map((n) => n.slice(0, -PROMPT_SUFFIX.length))
|
|
55
|
+
.sort();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Relay {
|
|
2
|
+
url: string;
|
|
3
|
+
close(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface RelaySpec {
|
|
6
|
+
/** the per-session secret copilot must present; anything else on the machine is refused */
|
|
7
|
+
key: string;
|
|
8
|
+
/** `https://…/v1/projects/<project>/locations/<location>` */
|
|
9
|
+
base: string;
|
|
10
|
+
token: () => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
export interface VertexRelaySpec {
|
|
13
|
+
key: string;
|
|
14
|
+
project: string;
|
|
15
|
+
location: string;
|
|
16
|
+
keyFile: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function startVertexRelay(spec: VertexRelaySpec): Promise<Relay>;
|
|
19
|
+
export declare function startRelay(spec: RelaySpec): Promise<Relay>;
|
|
20
|
+
//# sourceMappingURL=relay.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { vertexEndpoint } from '@zenera/neo';
|
|
2
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
const VERTEX_VERSION = 'vertex-2023-10-16';
|
|
7
|
+
export async function startVertexRelay(spec) {
|
|
8
|
+
const auth = new GoogleAuth({
|
|
9
|
+
keyFile: spec.keyFile,
|
|
10
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
11
|
+
});
|
|
12
|
+
const at = vertexEndpoint({
|
|
13
|
+
project: spec.project,
|
|
14
|
+
location: spec.location,
|
|
15
|
+
token: async () => {
|
|
16
|
+
const token = await auth.getAccessToken();
|
|
17
|
+
if (!token) {
|
|
18
|
+
throw new Error(`${spec.keyFile} minted no access token`);
|
|
19
|
+
}
|
|
20
|
+
return token;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return startRelay({ key: spec.key, base: at.base, token: at.token });
|
|
24
|
+
}
|
|
25
|
+
export async function startRelay(spec) {
|
|
26
|
+
const server = createServer((req, res) => {
|
|
27
|
+
relay(spec, req, res).catch((err) => {
|
|
28
|
+
fail(res, 502, 'api_error', `relay to vertex: ${err.message}`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
await new Promise((done, reject) => {
|
|
32
|
+
server.once('error', reject);
|
|
33
|
+
server.listen(0, '127.0.0.1', done);
|
|
34
|
+
});
|
|
35
|
+
const { port } = server.address();
|
|
36
|
+
return {
|
|
37
|
+
url: `http://127.0.0.1:${port}`,
|
|
38
|
+
close: () => new Promise((done) => {
|
|
39
|
+
server.closeAllConnections();
|
|
40
|
+
server.close(() => done());
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function relay(spec, req, res) {
|
|
45
|
+
if (!authorized(spec.key, req)) {
|
|
46
|
+
fail(res, 401, 'authentication_error', 'not this session’s relay key');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const path = new URL(req.url ?? '/', 'http://relay').pathname;
|
|
50
|
+
const counting = path === '/v1/messages/count_tokens';
|
|
51
|
+
if (req.method !== 'POST' || (path !== '/v1/messages' && !counting)) {
|
|
52
|
+
fail(res, 404, 'not_found_error', `vertex serves no ${req.method} ${path} for Claude`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(await readBody(req));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
fail(res, 400, 'invalid_request_error', 'the body is not JSON');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const { model, ...rest } = parsed;
|
|
64
|
+
if (!model) {
|
|
65
|
+
fail(res, 400, 'invalid_request_error', 'no model in the body');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const url = counting
|
|
69
|
+
? `${spec.base}/publishers/anthropic/models/count-tokens:rawPredict`
|
|
70
|
+
: `${spec.base}/publishers/anthropic/models/${encodeURIComponent(model)}:${rest.stream ? 'streamRawPredict' : 'rawPredict'}`;
|
|
71
|
+
const body = counting ? parsed : { ...rest, anthropic_version: VERTEX_VERSION };
|
|
72
|
+
const abort = new AbortController();
|
|
73
|
+
res.on('close', () => {
|
|
74
|
+
if (!res.writableFinished) {
|
|
75
|
+
abort.abort();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const beta = req.headers['anthropic-beta'];
|
|
79
|
+
const upstream = await fetch(url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
authorization: `Bearer ${await spec.token()}`,
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
...(typeof beta === 'string' ? { 'anthropic-beta': beta } : {}),
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
signal: abort.signal,
|
|
88
|
+
});
|
|
89
|
+
res.writeHead(upstream.status, {
|
|
90
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json',
|
|
91
|
+
});
|
|
92
|
+
if (!upstream.body) {
|
|
93
|
+
res.end();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
Readable.fromWeb(upstream.body)
|
|
97
|
+
.on('error', () => res.destroy())
|
|
98
|
+
.pipe(res);
|
|
99
|
+
}
|
|
100
|
+
function authorized(key, req) {
|
|
101
|
+
const header = req.headers['x-api-key'] ?? req.headers.authorization?.replace(/^Bearer /i, '');
|
|
102
|
+
const given = Buffer.from(typeof header === 'string' ? header : '');
|
|
103
|
+
const wanted = Buffer.from(key);
|
|
104
|
+
return given.length === wanted.length && timingSafeEqual(given, wanted);
|
|
105
|
+
}
|
|
106
|
+
function readBody(req) {
|
|
107
|
+
return new Promise((done, reject) => {
|
|
108
|
+
const chunks = [];
|
|
109
|
+
req.on('data', (c) => chunks.push(c));
|
|
110
|
+
req.on('end', () => done(Buffer.concat(chunks).toString('utf8')));
|
|
111
|
+
req.on('error', reject);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Anthropic's error shape, so copilot reports it as it would a direct call. */
|
|
115
|
+
function fail(res, code, type, message) {
|
|
116
|
+
if (res.headersSent) {
|
|
117
|
+
res.destroy();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.writeHead(code, { 'content-type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify({ type: 'error', error: { type, message } }));
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=relay.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ModelUsage, TokenUsage } from '@zenera/neo';
|
|
2
|
+
export interface MetaCall {
|
|
3
|
+
model: string;
|
|
4
|
+
usage: TokenUsage;
|
|
5
|
+
cacheWriteTokens?: number;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
session?: string;
|
|
9
|
+
traceId?: string;
|
|
10
|
+
spanId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One line of the span file, as a model call - anything else is undefined.
|
|
14
|
+
* Only counts are read off it: whatever else a span may carry, none of it
|
|
15
|
+
* reaches the ledger.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readSpan(line: string): MetaCall | undefined;
|
|
18
|
+
/** Whole lines out of a stream read in chunks; the unfinished tail is carried. */
|
|
19
|
+
export declare function splitLines(carry: string, chunk: string): {
|
|
20
|
+
lines: string[];
|
|
21
|
+
carry: string;
|
|
22
|
+
};
|
|
23
|
+
/** Reads a span file as it grows. `stop()` reads what is left, once. */
|
|
24
|
+
export declare function tailSpans(path: string, onCall: (call: MetaCall) => void, everyMs?: number): {
|
|
25
|
+
stop: () => void;
|
|
26
|
+
};
|
|
27
|
+
/** Calls and tokens so far, in the one line the status row and the summary share. */
|
|
28
|
+
export declare class Tally {
|
|
29
|
+
calls: number;
|
|
30
|
+
usage: TokenUsage;
|
|
31
|
+
add(call: MetaCall): void;
|
|
32
|
+
toString(): string;
|
|
33
|
+
}
|
|
34
|
+
/** 950, 12.3k, 1.8M. */
|
|
35
|
+
export declare function compact(n: number): string;
|
|
36
|
+
/**
|
|
37
|
+
* Copilot's own per-model totals from the last `session.shutdown` in a session's
|
|
38
|
+
* events - cumulative over every resume of it. Reasoning folded into output,
|
|
39
|
+
* as everywhere in zen.
|
|
40
|
+
*/
|
|
41
|
+
export declare function shutdownModels(events: string): ModelUsage[] | undefined;
|
|
42
|
+
export declare function sessionTotals(sessionId: string): ModelUsage[] | undefined;
|
|
43
|
+
/** Whether this machine holds the session, which is what `--resume` needs. */
|
|
44
|
+
export declare function sessionKept(sessionId: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The model a session's history was written by: its last answer's, else the one it started on.
|
|
47
|
+
* A failed resume on another model writes no answer, so it never becomes the session's model.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionModel(sessionId: string): string | undefined;
|
|
50
|
+
/** `google/gemini-3.8-flash` and `gemini-3.8-flash` are one model to copilot. */
|
|
51
|
+
export declare const sameModel: (a: string, b: string) => boolean;
|
|
52
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** An OTel time: `[seconds, nanoseconds]` in this exporter; epoch ms either way. */
|
|
5
|
+
function epochMs(t) {
|
|
6
|
+
if (Array.isArray(t) && typeof t[0] === 'number') {
|
|
7
|
+
return t[0] * 1000 + Number(t[1] ?? 0) / 1e6;
|
|
8
|
+
}
|
|
9
|
+
if (typeof t === 'number') {
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
12
|
+
return typeof t === 'string' ? Date.parse(t) : undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One line of the span file, as a model call - anything else is undefined.
|
|
16
|
+
* Only counts are read off it: whatever else a span may carry, none of it
|
|
17
|
+
* reaches the ledger.
|
|
18
|
+
*/
|
|
19
|
+
export function readSpan(line) {
|
|
20
|
+
let span;
|
|
21
|
+
try {
|
|
22
|
+
span = JSON.parse(line);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const attrs = (span.attributes ?? {});
|
|
28
|
+
if (span.type !== 'span' || attrs['gen_ai.operation.name'] !== 'chat') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const num = (key) => Number(attrs[key] ?? 0) || 0;
|
|
32
|
+
const start = epochMs(span.startTime) ?? 0;
|
|
33
|
+
const end = epochMs(span.endTime) ?? start;
|
|
34
|
+
// Copilot counts reasoning beside the output; zen counts it inside.
|
|
35
|
+
const reasoning = num('gen_ai.usage.reasoning.output_tokens');
|
|
36
|
+
const cacheWrite = num('gen_ai.usage.cache_creation.input_tokens') ||
|
|
37
|
+
num('gen_ai.usage.cache_creation_input_tokens');
|
|
38
|
+
return {
|
|
39
|
+
model: String(attrs['gen_ai.response.model'] ?? attrs['gen_ai.request.model'] ?? '?'),
|
|
40
|
+
usage: {
|
|
41
|
+
inputTokens: num('gen_ai.usage.input_tokens'),
|
|
42
|
+
cachedInputTokens: num('gen_ai.usage.cache_read.input_tokens') ||
|
|
43
|
+
num('gen_ai.usage.cache_read_input_tokens'),
|
|
44
|
+
outputTokens: num('gen_ai.usage.output_tokens') + reasoning,
|
|
45
|
+
reasoningTokens: reasoning,
|
|
46
|
+
},
|
|
47
|
+
...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),
|
|
48
|
+
startedAt: new Date(start).toISOString(),
|
|
49
|
+
durationMs: Math.max(0, Math.round(end - start)),
|
|
50
|
+
session: typeof attrs['gen_ai.conversation.id'] === 'string'
|
|
51
|
+
? attrs['gen_ai.conversation.id']
|
|
52
|
+
: undefined,
|
|
53
|
+
traceId: typeof span.traceId === 'string' ? span.traceId : undefined,
|
|
54
|
+
spanId: typeof span.spanId === 'string' ? span.spanId : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Whole lines out of a stream read in chunks; the unfinished tail is carried. */
|
|
58
|
+
export function splitLines(carry, chunk) {
|
|
59
|
+
const parts = `${carry}${chunk}`.split('\n');
|
|
60
|
+
const rest = parts.pop() ?? '';
|
|
61
|
+
return { lines: parts.filter((l) => l.trim()), carry: rest };
|
|
62
|
+
}
|
|
63
|
+
/** Reads a span file as it grows. `stop()` reads what is left, once. */
|
|
64
|
+
export function tailSpans(path, onCall, everyMs = 1000) {
|
|
65
|
+
let offset = 0;
|
|
66
|
+
let carry = '';
|
|
67
|
+
const poll = () => {
|
|
68
|
+
let fd;
|
|
69
|
+
try {
|
|
70
|
+
const size = statSync(path).size;
|
|
71
|
+
if (size <= offset) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
fd = openSync(path, 'r');
|
|
75
|
+
const buf = Buffer.alloc(size - offset);
|
|
76
|
+
readSync(fd, buf, 0, buf.length, offset);
|
|
77
|
+
offset = size;
|
|
78
|
+
const split = splitLines(carry, buf.toString('utf8'));
|
|
79
|
+
carry = split.carry;
|
|
80
|
+
for (const line of split.lines) {
|
|
81
|
+
const call = readSpan(line);
|
|
82
|
+
if (call) {
|
|
83
|
+
onCall(call);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Not there yet: copilot creates it at the first span.
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (fd !== undefined) {
|
|
92
|
+
closeSync(fd);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const timer = setInterval(poll, everyMs);
|
|
97
|
+
timer.unref();
|
|
98
|
+
return {
|
|
99
|
+
stop: () => {
|
|
100
|
+
clearInterval(timer);
|
|
101
|
+
poll();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Calls and tokens so far, in the one line the status row and the summary share. */
|
|
106
|
+
export class Tally {
|
|
107
|
+
calls = 0;
|
|
108
|
+
usage = {
|
|
109
|
+
inputTokens: 0,
|
|
110
|
+
cachedInputTokens: 0,
|
|
111
|
+
outputTokens: 0,
|
|
112
|
+
reasoningTokens: 0,
|
|
113
|
+
};
|
|
114
|
+
add(call) {
|
|
115
|
+
this.calls += 1;
|
|
116
|
+
this.usage = {
|
|
117
|
+
inputTokens: this.usage.inputTokens + call.usage.inputTokens,
|
|
118
|
+
cachedInputTokens: this.usage.cachedInputTokens + call.usage.cachedInputTokens,
|
|
119
|
+
outputTokens: this.usage.outputTokens + call.usage.outputTokens,
|
|
120
|
+
reasoningTokens: this.usage.reasoningTokens + call.usage.reasoningTokens,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
toString() {
|
|
124
|
+
const u = this.usage;
|
|
125
|
+
const cached = u.cachedInputTokens ? ` (${compact(u.cachedInputTokens)} cached)` : '';
|
|
126
|
+
return (`${this.calls} call${this.calls === 1 ? '' : 's'} · ${compact(u.inputTokens)} in${cached}` +
|
|
127
|
+
` · ${compact(u.outputTokens)} out`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** 950, 12.3k, 1.8M. */
|
|
131
|
+
export function compact(n) {
|
|
132
|
+
if (n < 1000) {
|
|
133
|
+
return String(n);
|
|
134
|
+
}
|
|
135
|
+
if (n < 1e6) {
|
|
136
|
+
return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
137
|
+
}
|
|
138
|
+
return `${(n / 1e6).toFixed(n < 1e7 ? 2 : 1)}M`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Copilot's own per-model totals from the last `session.shutdown` in a session's
|
|
142
|
+
* events - cumulative over every resume of it. Reasoning folded into output,
|
|
143
|
+
* as everywhere in zen.
|
|
144
|
+
*/
|
|
145
|
+
export function shutdownModels(events) {
|
|
146
|
+
const line = events
|
|
147
|
+
.split('\n')
|
|
148
|
+
.reverse()
|
|
149
|
+
.find((l) => l.includes('"session.shutdown"'));
|
|
150
|
+
if (!line) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
let metrics;
|
|
154
|
+
try {
|
|
155
|
+
metrics = JSON.parse(line).data?.modelMetrics ?? {};
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
return Object.entries(metrics).map(([model, m]) => {
|
|
161
|
+
const u = m.usage ?? {};
|
|
162
|
+
const reasoning = Number(u.reasoningTokens ?? 0);
|
|
163
|
+
return {
|
|
164
|
+
model,
|
|
165
|
+
calls: Number(m.requests?.count ?? 0),
|
|
166
|
+
usage: {
|
|
167
|
+
inputTokens: Number(u.inputTokens ?? 0),
|
|
168
|
+
cachedInputTokens: Number(u.cacheReadTokens ?? 0),
|
|
169
|
+
outputTokens: Number(u.outputTokens ?? 0) + reasoning,
|
|
170
|
+
reasoningTokens: reasoning,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export function sessionTotals(sessionId) {
|
|
176
|
+
const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
|
|
177
|
+
try {
|
|
178
|
+
return shutdownModels(readFileSync(join(home, 'session-state', sessionId, 'events.jsonl'), 'utf8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Whether this machine holds the session, which is what `--resume` needs. */
|
|
185
|
+
export function sessionKept(sessionId) {
|
|
186
|
+
const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
|
|
187
|
+
return existsSync(join(home, 'session-state', sessionId));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The model a session's history was written by: its last answer's, else the one it started on.
|
|
191
|
+
* A failed resume on another model writes no answer, so it never becomes the session's model.
|
|
192
|
+
*/
|
|
193
|
+
export function sessionModel(sessionId) {
|
|
194
|
+
const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
|
|
195
|
+
let text;
|
|
196
|
+
try {
|
|
197
|
+
text = readFileSync(join(home, 'session-state', sessionId, 'events.jsonl'), 'utf8');
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
let started;
|
|
203
|
+
const lines = text.split('\n');
|
|
204
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
205
|
+
const line = lines[i];
|
|
206
|
+
if (!line.includes('"assistant.message"') && !line.includes('"session.start"')) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const event = JSON.parse(line);
|
|
211
|
+
if (event.type === 'assistant.message' && typeof event.data?.model === 'string') {
|
|
212
|
+
return event.data.model;
|
|
213
|
+
}
|
|
214
|
+
if (event.type === 'session.start' && typeof event.data?.selectedModel === 'string') {
|
|
215
|
+
started = event.data.selectedModel;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// A line cut by a crash says nothing.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return started;
|
|
223
|
+
}
|
|
224
|
+
/** `google/gemini-3.8-flash` and `gemini-3.8-flash` are one model to copilot. */
|
|
225
|
+
export const sameModel = (a, b) => a.replace(/^.*\//, '').toLowerCase() === b.replace(/^.*\//, '').toLowerCase();
|
|
226
|
+
//# sourceMappingURL=tokens.js.map
|