@trydreamstate/cli 1.0.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 +62 -0
- package/dist/client.d.ts +106 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +265 -0
- package/dist/commands/sequences.d.ts +7 -0
- package/dist/commands/sequences.d.ts.map +1 -0
- package/dist/commands/sequences.js +44 -0
- package/dist/commands/shared.d.ts +53 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +243 -0
- package/dist/commands/workflows.d.ts +7 -0
- package/dist/commands/workflows.d.ts.map +1 -0
- package/dist/commands/workflows.js +51 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +144 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +896 -0
- package/dist/skillInstaller.d.ts +50 -0
- package/dist/skillInstaller.d.ts.map +1 -0
- package/dist/skillInstaller.js +189 -0
- package/dist/tableFilePreview.d.ts +12 -0
- package/dist/tableFilePreview.d.ts.map +1 -0
- package/dist/tableFilePreview.js +303 -0
- package/dist/turnCache.d.ts +8 -0
- package/dist/turnCache.d.ts.map +1 -0
- package/dist/turnCache.js +68 -0
- package/package.json +54 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const SKILL_INSTALL_AGENTS: readonly ["codex", "claude-code", "cursor"];
|
|
2
|
+
export type SkillInstallAgent = typeof SKILL_INSTALL_AGENTS[number];
|
|
3
|
+
export interface HostedSkill {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
body: string;
|
|
8
|
+
frontmatter?: Record<string, string | string[]>;
|
|
9
|
+
documents?: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface SkillInstallOptions {
|
|
15
|
+
agents: readonly SkillInstallAgent[];
|
|
16
|
+
homeDirectory?: string;
|
|
17
|
+
project?: boolean;
|
|
18
|
+
copy?: boolean;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
workspaceId?: string;
|
|
21
|
+
indexVersion?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function canonicalSkillsDirectory(homeDirectory?: string, project?: boolean, cwd?: string): string;
|
|
24
|
+
export declare function skillInstallDirectory(agent: SkillInstallAgent, homeDirectory?: string, project?: boolean, cwd?: string): string;
|
|
25
|
+
export declare function renderSkillMarkdown(skill: HostedSkill, provenance: {
|
|
26
|
+
workspaceId: string;
|
|
27
|
+
indexVersion: string;
|
|
28
|
+
}): string;
|
|
29
|
+
export declare function installSkills(skills: readonly HostedSkill[], options: SkillInstallOptions): Promise<{
|
|
30
|
+
files: string[];
|
|
31
|
+
lockSkills: Record<string, {
|
|
32
|
+
source: string;
|
|
33
|
+
sourceType: 'hosted';
|
|
34
|
+
computedHash: string;
|
|
35
|
+
}>;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function routerSkill(packsList: unknown): HostedSkill;
|
|
38
|
+
export declare function inspectSkillInstallers(agents: readonly SkillInstallAgent[], homeDirectory?: string, project?: boolean, cwd?: string): Promise<Array<{
|
|
39
|
+
agent: SkillInstallAgent;
|
|
40
|
+
directory: string;
|
|
41
|
+
writable: boolean;
|
|
42
|
+
canonical_directory: string;
|
|
43
|
+
symlinks: Array<{
|
|
44
|
+
name: string;
|
|
45
|
+
target: string;
|
|
46
|
+
correct: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
provenance_age_ms: number | null;
|
|
49
|
+
}>>;
|
|
50
|
+
//# sourceMappingURL=skillInstaller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillInstaller.d.ts","sourceRoot":"","sources":["../src/skillInstaller.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,oBAAoB,6CAA8C,CAAC;AAChF,MAAM,MAAM,iBAAiB,GAAG,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAKD,wBAAgB,wBAAwB,CAAC,aAAa,SAAY,EAAE,OAAO,UAAQ,EAAE,GAAG,SAAgB,GAAG,MAAM,CAEhH;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,iBAAiB,EAAE,aAAa,SAAY,EAAE,OAAO,UAAQ,EAAE,GAAG,SAAgB,GAAG,MAAM,CAKvI;AAiBD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAazH;AA8BD,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,QAAQ,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAe1N;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,OAAO,GAAG,WAAW,CAO3D;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,EAAE,aAAa,SAAY,EAAE,OAAO,UAAQ,EAAE,GAAG,SAAgB,GAAG,OAAO,CAAC,KAAK,CAAC;IAAE,KAAK,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC,CAkC1W"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SKILL_INSTALL_AGENTS = void 0;
|
|
37
|
+
exports.canonicalSkillsDirectory = canonicalSkillsDirectory;
|
|
38
|
+
exports.skillInstallDirectory = skillInstallDirectory;
|
|
39
|
+
exports.renderSkillMarkdown = renderSkillMarkdown;
|
|
40
|
+
exports.installSkills = installSkills;
|
|
41
|
+
exports.routerSkill = routerSkill;
|
|
42
|
+
exports.inspectSkillInstallers = inspectSkillInstallers;
|
|
43
|
+
const promises_1 = require("node:fs/promises");
|
|
44
|
+
const node_fs_1 = require("node:fs");
|
|
45
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
|
+
const node_os_1 = require("node:os");
|
|
47
|
+
const node_path_1 = require("node:path");
|
|
48
|
+
const index_1 = require("./index");
|
|
49
|
+
exports.SKILL_INSTALL_AGENTS = ['codex', 'claude-code', 'cursor'];
|
|
50
|
+
const allowedTools = 'Bash(dreamstate *), Bash(jq *)';
|
|
51
|
+
const safeId = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
52
|
+
function canonicalSkillsDirectory(homeDirectory = (0, node_os_1.homedir)(), project = false, cwd = process.cwd()) {
|
|
53
|
+
return project ? (0, node_path_1.join)(cwd, '.agents', 'skills') : (0, node_path_1.join)(homeDirectory, '.agents', 'skills');
|
|
54
|
+
}
|
|
55
|
+
function skillInstallDirectory(agent, homeDirectory = (0, node_os_1.homedir)(), project = false, cwd = process.cwd()) {
|
|
56
|
+
// Codex and Cursor both read the shared .agents/skills directory; only Claude
|
|
57
|
+
// Code needs its own directory, which links back into the shared one.
|
|
58
|
+
if (agent !== 'claude-code')
|
|
59
|
+
return canonicalSkillsDirectory(homeDirectory, project, cwd);
|
|
60
|
+
return (0, node_path_1.join)(project ? cwd : homeDirectory, '.claude', 'skills');
|
|
61
|
+
}
|
|
62
|
+
function yamlValue(value) { return JSON.stringify(value); }
|
|
63
|
+
function skillName(id) {
|
|
64
|
+
if (!safeId.test(id))
|
|
65
|
+
throw new index_1.CliUsageError('Dreamstate API returned an invalid skill id.');
|
|
66
|
+
return id === 'router' ? 'dreamstate' : `dreamstate-${id}`;
|
|
67
|
+
}
|
|
68
|
+
function commandGuide(packs) {
|
|
69
|
+
return `## How to run this in Dreamstate\n\nUse \`dreamstate whoami --json\` (MCP \`whoami\`) to verify workspace. Route ambiguous work with \`dreamstate packs route --goal "<goal>" --json\` (MCP \`packs_route\`). Discover actions with \`dreamstate tools search --goal "<goal>" --json\` (MCP \`tools_search\`), inspect exact input with \`dreamstate tools get <capability_id> --json\` (MCP \`tools_get\`), then execute with \`dreamstate tools run <capability_id> --params '{...}' --mode <dry_run|live> --json\` (MCP \`tools_run\`). Check asynchronous work with \`dreamstate runs get <run_id> --json\` (MCP \`runs_get\`).\n\nGoals: ${packs.length ? packs.join(', ') : 'not declared'}.`;
|
|
70
|
+
}
|
|
71
|
+
function packs(frontmatter) {
|
|
72
|
+
const value = frontmatter.packs;
|
|
73
|
+
return Array.isArray(value) ? value : typeof value === 'string' ? value.split(',').map((item) => item.trim()).filter(Boolean) : [];
|
|
74
|
+
}
|
|
75
|
+
function renderSkillMarkdown(skill, provenance) {
|
|
76
|
+
const name = skillName(skill.id);
|
|
77
|
+
const source = skill.frontmatter ?? {};
|
|
78
|
+
const frontmatter = {
|
|
79
|
+
...source,
|
|
80
|
+
name,
|
|
81
|
+
description: skill.description,
|
|
82
|
+
'allowed-tools': allowedTools,
|
|
83
|
+
};
|
|
84
|
+
const lines = Object.entries(frontmatter).flatMap(([key, value]) => Array.isArray(value)
|
|
85
|
+
? [`${key}: [${value.map(yamlValue).join(', ')}]`]
|
|
86
|
+
: [`${key}: ${yamlValue(value)}`]);
|
|
87
|
+
return `---\n${lines.join('\n')}\n---\n\n${commandGuide(packs(frontmatter))}\n\n${skill.body.trim()}\n\n<!-- dreamstate provenance: workspace=${provenance.workspaceId} issued=${new Date().toISOString()} skills=${provenance.indexVersion} -->\n`;
|
|
88
|
+
}
|
|
89
|
+
function hash(value) { return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex'); }
|
|
90
|
+
async function writeHostedSkill(root, skill, provenance) {
|
|
91
|
+
const name = skillName(skill.id);
|
|
92
|
+
const directory = (0, node_path_1.join)(root, name);
|
|
93
|
+
const markdown = renderSkillMarkdown(skill, provenance);
|
|
94
|
+
await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
|
|
95
|
+
const file = (0, node_path_1.join)(directory, 'SKILL.md');
|
|
96
|
+
await (0, promises_1.writeFile)(file, markdown, { encoding: 'utf8', mode: 0o600 });
|
|
97
|
+
for (const document of skill.documents ?? []) {
|
|
98
|
+
if (!safeId.test(document.name.replace(/\.[a-z0-9]+$/i, '')))
|
|
99
|
+
continue;
|
|
100
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(directory, document.name), document.content, { encoding: 'utf8', mode: 0o600 });
|
|
101
|
+
}
|
|
102
|
+
return { file, name, computedHash: hash(markdown) };
|
|
103
|
+
}
|
|
104
|
+
async function linkAgentSkill(agentRoot, canonicalDirectory, name, copy) {
|
|
105
|
+
const targetDirectory = (0, node_path_1.join)(agentRoot, name);
|
|
106
|
+
await (0, promises_1.mkdir)(agentRoot, { recursive: true, mode: 0o700 });
|
|
107
|
+
await (0, promises_1.rm)(targetDirectory, { recursive: true, force: true });
|
|
108
|
+
if (copy) {
|
|
109
|
+
await (0, promises_1.mkdir)(targetDirectory, { recursive: true, mode: 0o700 });
|
|
110
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(targetDirectory, 'SKILL.md'), await (0, promises_1.readFile)((0, node_path_1.join)(canonicalDirectory, name, 'SKILL.md')), { encoding: 'utf8', mode: 0o600 });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
await (0, promises_1.symlink)((0, node_path_1.relative)(agentRoot, (0, node_path_1.join)(canonicalDirectory, name)), targetDirectory, 'dir');
|
|
114
|
+
}
|
|
115
|
+
return (0, node_path_1.join)(targetDirectory, 'SKILL.md');
|
|
116
|
+
}
|
|
117
|
+
async function installSkills(skills, options) {
|
|
118
|
+
const home = options.homeDirectory ?? (0, node_os_1.homedir)();
|
|
119
|
+
const cwd = options.cwd ?? process.cwd();
|
|
120
|
+
const canonical = canonicalSkillsDirectory(home, Boolean(options.project), cwd);
|
|
121
|
+
await (0, promises_1.mkdir)(canonical, { recursive: true, mode: 0o700 });
|
|
122
|
+
const provenance = { workspaceId: options.workspaceId ?? 'unknown', indexVersion: options.indexVersion ?? 'unknown' };
|
|
123
|
+
const entries = await Promise.all(skills.map((skill) => writeHostedSkill(canonical, skill, provenance)));
|
|
124
|
+
const files = entries.map((entry) => entry.file);
|
|
125
|
+
for (const agent of options.agents.filter((agent) => agent === 'claude-code')) {
|
|
126
|
+
const root = skillInstallDirectory(agent, home, Boolean(options.project), cwd);
|
|
127
|
+
for (const entry of entries)
|
|
128
|
+
files.push(await linkAgentSkill(root, canonical, entry.name, Boolean(options.copy)));
|
|
129
|
+
}
|
|
130
|
+
const lockSkills = Object.fromEntries(entries.map((entry, index) => [entry.name, { source: skills[index].id, sourceType: 'hosted', computedHash: entry.computedHash }]));
|
|
131
|
+
if (options.project)
|
|
132
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(cwd, 'skills-lock.json'), `${JSON.stringify({ version: 1, skills: lockSkills }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
133
|
+
return { files, lockSkills };
|
|
134
|
+
}
|
|
135
|
+
function routerSkill(packsList) {
|
|
136
|
+
const root = packsList && typeof packsList === 'object' ? packsList : {};
|
|
137
|
+
const records = Array.isArray(root.data?.packs) ? root.data.packs : [];
|
|
138
|
+
const goals = records.map((item) => item && typeof item === 'object' ? item : {})
|
|
139
|
+
.filter((item) => typeof item.id === 'string');
|
|
140
|
+
const lines = goals.map((pack) => `- ${pack.id}: ${pack.owns ?? 'Dreamstate capability pack.'}${pack.not_for ? ` ${pack.not_for}` : ''}`).join('\n');
|
|
141
|
+
return { id: 'router', name: 'Dreamstate', description: 'Route work to Dreamstate capability skills.', body: `# Dreamstate router\n\nPrefer narrower \`dreamstate-*\` skill when one fits. Otherwise route with \`dreamstate packs route --goal "<goal>" --json\` (MCP \`packs_route\`), search tools scoped by the returned pack with \`dreamstate tools search --goal "<goal>" --toolset <pack_id> --json\` (MCP \`tools_search\`), fetch exact schema with \`dreamstate tools get <capability_id> --json\` (MCP \`tools_get\`), then run it with \`dreamstate tools run <capability_id> --params '{...}' --json\` (MCP \`tools_run\`).\n\n## Goals\n\n${lines || 'No packs returned by GET /packs.'}`, frontmatter: { packs: goals.map((pack) => pack.id) } };
|
|
142
|
+
}
|
|
143
|
+
async function inspectSkillInstallers(agents, homeDirectory = (0, node_os_1.homedir)(), project = false, cwd = process.cwd()) {
|
|
144
|
+
const canonical = canonicalSkillsDirectory(homeDirectory, project, cwd);
|
|
145
|
+
let canonicalExists = false;
|
|
146
|
+
try {
|
|
147
|
+
canonicalExists = (await (0, promises_1.stat)(canonical)).isDirectory();
|
|
148
|
+
}
|
|
149
|
+
catch { /* absent */ }
|
|
150
|
+
return Promise.all(agents.map(async (agent) => {
|
|
151
|
+
const directory = skillInstallDirectory(agent, homeDirectory, project, cwd);
|
|
152
|
+
let writable = false;
|
|
153
|
+
try {
|
|
154
|
+
writable = (await (0, promises_1.stat)(directory)).isDirectory();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
try {
|
|
158
|
+
await (0, promises_1.access)((0, node_path_1.dirname)(directory), node_fs_1.constants.W_OK);
|
|
159
|
+
writable = true;
|
|
160
|
+
}
|
|
161
|
+
catch { /* unavailable */ }
|
|
162
|
+
}
|
|
163
|
+
const symlinks = [];
|
|
164
|
+
if (agent === 'claude-code') {
|
|
165
|
+
try {
|
|
166
|
+
for (const name of await (await Promise.resolve().then(() => __importStar(require('node:fs/promises')))).readdir(directory)) {
|
|
167
|
+
const path = (0, node_path_1.join)(directory, name);
|
|
168
|
+
const state = await (0, promises_1.lstat)(path);
|
|
169
|
+
if (state.isSymbolicLink()) {
|
|
170
|
+
const target = await (await Promise.resolve().then(() => __importStar(require('node:fs/promises')))).readlink(path);
|
|
171
|
+
symlinks.push({ name, target, correct: (0, node_path_1.join)(directory, target) === (0, node_path_1.join)(canonical, name) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { /* absent */ }
|
|
176
|
+
}
|
|
177
|
+
let provenance_age_ms = null;
|
|
178
|
+
if (canonicalExists) {
|
|
179
|
+
try {
|
|
180
|
+
const router = await (0, promises_1.readFile)((0, node_path_1.join)(canonical, 'dreamstate', 'SKILL.md'), 'utf8');
|
|
181
|
+
const issued = router.match(/<!-- dreamstate provenance: workspace=[^\s]+ issued=([^\s]+) skills=[^>]+ -->/);
|
|
182
|
+
const timestamp = issued?.[1] ? Date.parse(issued[1]) : Number.NaN;
|
|
183
|
+
provenance_age_ms = Number.isFinite(timestamp) ? Date.now() - timestamp : null;
|
|
184
|
+
}
|
|
185
|
+
catch { /* absent */ }
|
|
186
|
+
}
|
|
187
|
+
return { agent, directory, writable, canonical_directory: canonical, symlinks, provenance_age_ms };
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type LocalTableFileKind = 'csv' | 'json' | 'jsonl' | 'xlsx';
|
|
2
|
+
export interface LocalTableFilePreview {
|
|
3
|
+
file_kind: LocalTableFileKind;
|
|
4
|
+
headers: string[];
|
|
5
|
+
sample_rows: Array<Record<string, string>>;
|
|
6
|
+
suggested_mapping: Record<string, string>;
|
|
7
|
+
truncated: boolean;
|
|
8
|
+
writes: false;
|
|
9
|
+
}
|
|
10
|
+
export declare function inferLocalTableFileKind(filePath: string, format?: string): LocalTableFileKind;
|
|
11
|
+
export declare function previewLocalTableFile(filePath: string, format?: string): Promise<LocalTableFilePreview>;
|
|
12
|
+
//# sourceMappingURL=tableFilePreview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tableFilePreview.d.ts","sourceRoot":"","sources":["../src/tableFilePreview.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,KAAK,CAAC;CACf;AAgBD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAK7F;AAoOD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAgB7G"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferLocalTableFileKind = inferLocalTableFileKind;
|
|
4
|
+
exports.previewLocalTableFile = previewLocalTableFile;
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_zlib_1 = require("node:zlib");
|
|
7
|
+
const SAMPLE_ROWS = 5;
|
|
8
|
+
const TEXT_PREVIEW_BYTES = 1_000_000;
|
|
9
|
+
const XLSX_PREVIEW_BYTES = 20_000_000;
|
|
10
|
+
const MAPPABLE_FIELDS = [
|
|
11
|
+
'external_id', 'name', 'first_name', 'last_name', 'company',
|
|
12
|
+
'title', 'headline', 'email', 'linkedin_url', 'profile_url', 'avatar_url',
|
|
13
|
+
];
|
|
14
|
+
function normalizeHeaderKey(value) {
|
|
15
|
+
return value.trim().toLowerCase().replace(/[\s_-]+/g, '');
|
|
16
|
+
}
|
|
17
|
+
function suggestedMapping(headers) {
|
|
18
|
+
const byNormalized = new Map(MAPPABLE_FIELDS.map((field) => [normalizeHeaderKey(field), field]));
|
|
19
|
+
const mapping = {};
|
|
20
|
+
for (const header of headers) {
|
|
21
|
+
const match = byNormalized.get(normalizeHeaderKey(header));
|
|
22
|
+
if (match)
|
|
23
|
+
mapping[header] = match;
|
|
24
|
+
}
|
|
25
|
+
return mapping;
|
|
26
|
+
}
|
|
27
|
+
function inferLocalTableFileKind(filePath, format) {
|
|
28
|
+
if (format === 'csv' || format === 'json' || format === 'jsonl' || format === 'xlsx')
|
|
29
|
+
return format;
|
|
30
|
+
const extension = filePath.split('.').pop()?.toLowerCase();
|
|
31
|
+
if (extension === 'json' || extension === 'jsonl' || extension === 'xlsx')
|
|
32
|
+
return extension;
|
|
33
|
+
return 'csv';
|
|
34
|
+
}
|
|
35
|
+
function finish(kind, headers, dataRows, truncated) {
|
|
36
|
+
const usable = headers.map((header) => header.trim());
|
|
37
|
+
if (usable.length === 0 || usable.every((header) => header.length === 0)) {
|
|
38
|
+
throw new Error('File has no header row');
|
|
39
|
+
}
|
|
40
|
+
const sample = dataRows.slice(0, SAMPLE_ROWS).map((row) => {
|
|
41
|
+
const record = {};
|
|
42
|
+
usable.forEach((header, index) => {
|
|
43
|
+
if (header.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
record[header] = row[index] ?? '';
|
|
46
|
+
});
|
|
47
|
+
return record;
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
file_kind: kind,
|
|
51
|
+
headers: usable.filter((header) => header.length > 0),
|
|
52
|
+
sample_rows: sample,
|
|
53
|
+
suggested_mapping: suggestedMapping(usable),
|
|
54
|
+
truncated: truncated || dataRows.length > SAMPLE_ROWS,
|
|
55
|
+
writes: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function parseCsv(text) {
|
|
59
|
+
const rows = [];
|
|
60
|
+
let row = [];
|
|
61
|
+
let field = '';
|
|
62
|
+
let quoted = false;
|
|
63
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
64
|
+
const char = text[index];
|
|
65
|
+
if (quoted) {
|
|
66
|
+
if (char === '"') {
|
|
67
|
+
if (text[index + 1] === '"') {
|
|
68
|
+
field += '"';
|
|
69
|
+
index += 1;
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
quoted = false;
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
field += char;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === '"') {
|
|
79
|
+
quoted = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === ',') {
|
|
83
|
+
row.push(field);
|
|
84
|
+
field = '';
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '\n') {
|
|
88
|
+
row.push(field);
|
|
89
|
+
field = '';
|
|
90
|
+
if (!(row.length === 1 && row[0] === ''))
|
|
91
|
+
rows.push(row);
|
|
92
|
+
row = [];
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char !== '\r')
|
|
96
|
+
field += char;
|
|
97
|
+
}
|
|
98
|
+
const endedClean = !quoted && field.length === 0 && row.length === 0;
|
|
99
|
+
if (field.length > 0 || row.length > 0) {
|
|
100
|
+
row.push(field);
|
|
101
|
+
if (!(row.length === 1 && row[0] === ''))
|
|
102
|
+
rows.push(row);
|
|
103
|
+
}
|
|
104
|
+
return { rows, endedClean };
|
|
105
|
+
}
|
|
106
|
+
function previewDelimited(kind, text, clipped) {
|
|
107
|
+
const parsed = parseCsv(text);
|
|
108
|
+
const rows = parsed.rows;
|
|
109
|
+
if (clipped && !parsed.endedClean)
|
|
110
|
+
rows.pop();
|
|
111
|
+
if (rows.length === 0)
|
|
112
|
+
throw new Error('File has no header row');
|
|
113
|
+
const [headers, ...dataRows] = rows;
|
|
114
|
+
return finish(kind, headers ?? [], dataRows, clipped);
|
|
115
|
+
}
|
|
116
|
+
function cellString(value) {
|
|
117
|
+
if (value === null || value === undefined)
|
|
118
|
+
return '';
|
|
119
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
120
|
+
return String(value);
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
function previewObjects(kind, records) {
|
|
124
|
+
const objects = records.filter((record) => record && typeof record === 'object' && !Array.isArray(record));
|
|
125
|
+
if (objects.length === 0)
|
|
126
|
+
throw new Error('JSON import expects an object or an array of objects');
|
|
127
|
+
const headers = [];
|
|
128
|
+
for (const record of objects.slice(0, SAMPLE_ROWS + 1)) {
|
|
129
|
+
for (const key of Object.keys(record)) {
|
|
130
|
+
if (!headers.includes(key))
|
|
131
|
+
headers.push(key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const dataRows = objects.map((record) => headers.map((header) => cellString(record[header])));
|
|
135
|
+
return finish(kind, headers, dataRows, objects.length > SAMPLE_ROWS);
|
|
136
|
+
}
|
|
137
|
+
function previewJson(text) {
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(text);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new Error(`JSON preview could not parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(parsed))
|
|
146
|
+
return previewObjects('json', parsed);
|
|
147
|
+
if (parsed && typeof parsed === 'object')
|
|
148
|
+
return previewObjects('json', [parsed]);
|
|
149
|
+
throw new Error('JSON import expects an object or an array of objects');
|
|
150
|
+
}
|
|
151
|
+
function previewJsonl(text, clipped) {
|
|
152
|
+
const lines = text.split(/\r?\n/);
|
|
153
|
+
if (clipped)
|
|
154
|
+
lines.pop();
|
|
155
|
+
const records = [];
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
const trimmed = line.trim();
|
|
158
|
+
if (!trimmed)
|
|
159
|
+
continue;
|
|
160
|
+
try {
|
|
161
|
+
records.push(JSON.parse(trimmed));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new Error(`JSONL preview could not parse a line: ${error instanceof Error ? error.message : String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
if (records.length > SAMPLE_ROWS)
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return previewObjects('jsonl', records);
|
|
170
|
+
}
|
|
171
|
+
function decodeXml(value) {
|
|
172
|
+
return value
|
|
173
|
+
.replace(/</g, '<')
|
|
174
|
+
.replace(/>/g, '>')
|
|
175
|
+
.replace(/"/g, '"')
|
|
176
|
+
.replace(/'/g, "'")
|
|
177
|
+
.replace(/&/g, '&');
|
|
178
|
+
}
|
|
179
|
+
function unzipEntries(buffer) {
|
|
180
|
+
const eocdMin = Math.max(0, buffer.length - 22 - 65535);
|
|
181
|
+
let eocd = -1;
|
|
182
|
+
for (let index = buffer.length - 22; index >= eocdMin; index -= 1) {
|
|
183
|
+
if (buffer.readUInt32LE(index) === 0x06054b50) {
|
|
184
|
+
eocd = index;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (eocd < 0)
|
|
189
|
+
throw new Error('xlsx file has no zip directory');
|
|
190
|
+
const out = new Map();
|
|
191
|
+
let offset = buffer.readUInt32LE(eocd + 16);
|
|
192
|
+
const entryCount = buffer.readUInt16LE(eocd + 10);
|
|
193
|
+
for (let index = 0; index < entryCount && offset + 46 <= buffer.length; index += 1) {
|
|
194
|
+
if (buffer.readUInt32LE(offset) !== 0x02014b50)
|
|
195
|
+
break;
|
|
196
|
+
const method = buffer.readUInt16LE(offset + 10);
|
|
197
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
198
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
199
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
200
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
201
|
+
const localOffset = buffer.readUInt32LE(offset + 42);
|
|
202
|
+
const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLength);
|
|
203
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
204
|
+
if (!/xl\/(sharedStrings\.xml|worksheets\/sheet\d+\.xml)$/.test(name))
|
|
205
|
+
continue;
|
|
206
|
+
if (localOffset + 30 > buffer.length)
|
|
207
|
+
throw new Error('xlsx local header is outside the file');
|
|
208
|
+
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
209
|
+
const localExtraLength = buffer.readUInt16LE(localOffset + 28);
|
|
210
|
+
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
|
211
|
+
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
212
|
+
if (method === 0)
|
|
213
|
+
out.set(name, Buffer.from(compressed));
|
|
214
|
+
else if (method === 8)
|
|
215
|
+
out.set(name, (0, node_zlib_1.inflateRawSync)(compressed));
|
|
216
|
+
else
|
|
217
|
+
throw new Error(`Unsupported xlsx compression method ${method}`);
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
function columnIndex(ref) {
|
|
222
|
+
const letters = /^[A-Z]+/i.exec(ref)?.[0] ?? '';
|
|
223
|
+
let index = 0;
|
|
224
|
+
for (const char of letters.toUpperCase())
|
|
225
|
+
index = index * 26 + (char.charCodeAt(0) - 64);
|
|
226
|
+
return index - 1;
|
|
227
|
+
}
|
|
228
|
+
function xmlTexts(fragment) {
|
|
229
|
+
return [...fragment.matchAll(/<(?:[\w-]+:)?t\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?t>/g)]
|
|
230
|
+
.map((match) => decodeXml(match[1] ?? ''))
|
|
231
|
+
.join('');
|
|
232
|
+
}
|
|
233
|
+
function sharedStringTable(xml) {
|
|
234
|
+
return [...xml.matchAll(/<(?:[\w-]+:)?si\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?si>/g)]
|
|
235
|
+
.map((match) => xmlTexts(match[1] ?? ''));
|
|
236
|
+
}
|
|
237
|
+
function previewXlsx(buffer) {
|
|
238
|
+
const entries = unzipEntries(buffer);
|
|
239
|
+
const sheetName = [...entries.keys()]
|
|
240
|
+
.filter((name) => /xl\/worksheets\/sheet\d+\.xml$/.test(name))
|
|
241
|
+
.sort()[0];
|
|
242
|
+
if (!sheetName)
|
|
243
|
+
throw new Error('xlsx preview could not find a worksheet');
|
|
244
|
+
const sheet = entries.get(sheetName).toString('utf8');
|
|
245
|
+
const sharedName = [...entries.keys()].find((name) => name.endsWith('xl/sharedStrings.xml'));
|
|
246
|
+
const shared = sharedName ? sharedStringTable(entries.get(sharedName).toString('utf8')) : [];
|
|
247
|
+
const grid = new Map();
|
|
248
|
+
for (const rowMatch of sheet.matchAll(/<(?:[\w-]+:)?row\b([^>]*)>([\s\S]*?)<\/(?:[\w-]+:)?row>/g)) {
|
|
249
|
+
const rowRef = /\br="(\d+)"/.exec(rowMatch[1] ?? '');
|
|
250
|
+
const rowNumber = rowRef ? Number(rowRef[1]) : grid.size + 1;
|
|
251
|
+
if (rowNumber > SAMPLE_ROWS + 1)
|
|
252
|
+
continue;
|
|
253
|
+
const cells = new Map();
|
|
254
|
+
for (const cellMatch of (rowMatch[2] ?? '').matchAll(/<(?:[\w-]+:)?c\b([^>]*)(?:\/>|>([\s\S]*?)<\/(?:[\w-]+:)?c>)/g)) {
|
|
255
|
+
const attrs = cellMatch[1] ?? '';
|
|
256
|
+
const ref = /\br="([A-Z]+\d+)"/i.exec(attrs);
|
|
257
|
+
const type = /\bt="([^"]+)"/.exec(attrs)?.[1] ?? '';
|
|
258
|
+
const body = cellMatch[2] ?? '';
|
|
259
|
+
const column = columnIndex(ref?.[1] ?? '');
|
|
260
|
+
if (column < 0)
|
|
261
|
+
continue;
|
|
262
|
+
let value = '';
|
|
263
|
+
if (type === 's') {
|
|
264
|
+
const index = Number(/<(?:[\w-]+:)?v\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?v>/.exec(body)?.[1] ?? '');
|
|
265
|
+
value = shared[index] ?? '';
|
|
266
|
+
}
|
|
267
|
+
else if (type === 'inlineStr')
|
|
268
|
+
value = xmlTexts(body);
|
|
269
|
+
else
|
|
270
|
+
value = decodeXml(/<(?:[\w-]+:)?v\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?v>/.exec(body)?.[1] ?? '');
|
|
271
|
+
cells.set(column, value);
|
|
272
|
+
}
|
|
273
|
+
grid.set(rowNumber, cells);
|
|
274
|
+
}
|
|
275
|
+
const rowNumbers = [...grid.keys()].sort((left, right) => left - right);
|
|
276
|
+
if (rowNumbers.length === 0)
|
|
277
|
+
throw new Error('File has no header row');
|
|
278
|
+
const width = Math.max(...[...grid.values()].map((cells) => Math.max(-1, ...cells.keys()))) + 1;
|
|
279
|
+
const toRow = (cells) => Array.from({ length: width }, (_, index) => cells?.get(index) ?? '');
|
|
280
|
+
const [headerNumber, ...dataNumbers] = rowNumbers;
|
|
281
|
+
const rowCount = [...sheet.matchAll(/<(?:[\w-]+:)?row\b/g)].length;
|
|
282
|
+
return finish('xlsx', toRow(grid.get(headerNumber)), dataNumbers.map((number) => toRow(grid.get(number))), rowCount > SAMPLE_ROWS + 1);
|
|
283
|
+
}
|
|
284
|
+
async function previewLocalTableFile(filePath, format) {
|
|
285
|
+
const kind = inferLocalTableFileKind(filePath, format);
|
|
286
|
+
if (kind === 'xlsx') {
|
|
287
|
+
const file = await (0, promises_1.readFile)(filePath);
|
|
288
|
+
if (file.length > XLSX_PREVIEW_BYTES)
|
|
289
|
+
throw new Error('xlsx preview refused a file larger than 20MB');
|
|
290
|
+
return previewXlsx(file);
|
|
291
|
+
}
|
|
292
|
+
const handle = await (0, promises_1.readFile)(filePath);
|
|
293
|
+
const clipped = handle.length > TEXT_PREVIEW_BYTES;
|
|
294
|
+
const text = handle.subarray(0, TEXT_PREVIEW_BYTES).toString('utf8');
|
|
295
|
+
if (kind === 'json') {
|
|
296
|
+
if (clipped)
|
|
297
|
+
throw new Error('JSON preview refused a file larger than 1MB');
|
|
298
|
+
return previewJson(text);
|
|
299
|
+
}
|
|
300
|
+
if (kind === 'jsonl')
|
|
301
|
+
return previewJsonl(text, clipped);
|
|
302
|
+
return previewDelimited(kind, text, clipped);
|
|
303
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const TURN_REUSE_MS: number;
|
|
2
|
+
type CacheWarning = (error: NodeJS.ErrnoException) => void;
|
|
3
|
+
export declare function turnCachePath(credentialsPath: string): string;
|
|
4
|
+
export declare function reusableTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, now?: number, onWarning?: CacheWarning): Promise<string | null>;
|
|
5
|
+
export declare function rememberTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, token: string, now?: number, onWarning?: CacheWarning): Promise<void>;
|
|
6
|
+
export declare function forgetTurnToken(path: string, baseUrl: string, apiKey: string, capabilityId: string, onWarning?: CacheWarning): Promise<void>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=turnCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnCache.d.ts","sourceRoot":"","sources":["../src/turnCache.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,aAAa,QAAa,CAAC;AAGxC,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,cAAc,KAAK,IAAI,CAAC;AAE3D,wBAAgB,aAAa,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE7D;AA2BD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,SAAS,CAAC,EAAE,YAAY,GAC9G,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAa,EAAE,SAAS,CAAC,EAAE,YAAY,GAC7H,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,YAAY,GAC5F,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TURN_REUSE_MS = void 0;
|
|
4
|
+
exports.turnCachePath = turnCachePath;
|
|
5
|
+
exports.reusableTurnToken = reusableTurnToken;
|
|
6
|
+
exports.rememberTurnToken = rememberTurnToken;
|
|
7
|
+
exports.forgetTurnToken = forgetTurnToken;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const promises_1 = require("node:fs/promises");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
// A paid capability's first live call is refused with quote_required and the
|
|
12
|
+
// server binds a quote to that call's tool_turn_token. Only a rerun carrying
|
|
13
|
+
// the SAME token can spend. Each CLI invocation is a new process, so fetching
|
|
14
|
+
// a fresh token per run made every paid capability unrunnable from the CLI
|
|
15
|
+
// (2026-09-23: tables.table_sources.run refused four times in a row), while
|
|
16
|
+
// the refusal itself told the user the CLI reuses the turn for 10 minutes.
|
|
17
|
+
// The server turn lives 10 minutes; reuse for 9 so a cached token never
|
|
18
|
+
// expires mid-request.
|
|
19
|
+
exports.TURN_REUSE_MS = 9 * 60_000;
|
|
20
|
+
function turnCachePath(credentialsPath) {
|
|
21
|
+
return (0, node_path_1.join)((0, node_path_1.dirname)(credentialsPath), 'turns.json');
|
|
22
|
+
}
|
|
23
|
+
function entryKey(baseUrl, apiKey, capabilityId) {
|
|
24
|
+
// The key fingerprint keeps two workspaces on one machine from sharing a turn.
|
|
25
|
+
const fingerprint = (0, node_crypto_1.createHash)('sha256').update(apiKey).digest('hex').slice(0, 16);
|
|
26
|
+
return `${baseUrl} ${fingerprint} ${capabilityId}`;
|
|
27
|
+
}
|
|
28
|
+
async function load(path, onWarning) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(await (0, promises_1.readFile)(path, 'utf8'));
|
|
31
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== 'ENOENT')
|
|
35
|
+
onWarning?.(error);
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function persist(path, entries, onWarning) {
|
|
40
|
+
try {
|
|
41
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
|
|
42
|
+
await (0, promises_1.writeFile)(path, JSON.stringify(entries), { mode: 0o600 });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
onWarning?.(error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function reusableTurnToken(path, baseUrl, apiKey, capabilityId, now = Date.now(), onWarning) {
|
|
49
|
+
const entry = (await load(path, onWarning))[entryKey(baseUrl, apiKey, capabilityId)];
|
|
50
|
+
return entry && typeof entry.token === 'string' && now - entry.at < exports.TURN_REUSE_MS ? entry.token : null;
|
|
51
|
+
}
|
|
52
|
+
async function rememberTurnToken(path, baseUrl, apiKey, capabilityId, token, now = Date.now(), onWarning) {
|
|
53
|
+
const entries = await load(path, onWarning);
|
|
54
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
55
|
+
if (!entry || now - entry.at >= exports.TURN_REUSE_MS)
|
|
56
|
+
delete entries[key];
|
|
57
|
+
}
|
|
58
|
+
entries[entryKey(baseUrl, apiKey, capabilityId)] = { token, at: now };
|
|
59
|
+
await persist(path, entries, onWarning);
|
|
60
|
+
}
|
|
61
|
+
async function forgetTurnToken(path, baseUrl, apiKey, capabilityId, onWarning) {
|
|
62
|
+
const entries = await load(path, onWarning);
|
|
63
|
+
const key = entryKey(baseUrl, apiKey, capabilityId);
|
|
64
|
+
if (!(key in entries))
|
|
65
|
+
return;
|
|
66
|
+
delete entries[key];
|
|
67
|
+
await persist(path, entries, onWarning);
|
|
68
|
+
}
|