@keystrokehq/cli 0.1.22 → 0.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/dist/{dist-UdbUy9DM.mjs → dist-BOhrc_Nv.mjs} +20 -2
- package/dist/{dist-UdbUy9DM.mjs.map → dist-BOhrc_Nv.mjs.map} +1 -1
- package/dist/{dist-Bwn5tAiT.mjs → dist-C1QOfWMM.mjs} +206 -8545
- package/dist/dist-C1QOfWMM.mjs.map +1 -0
- package/dist/{dist-BdewSOMb.mjs → dist-CUEVu120.mjs} +3 -3
- package/dist/{dist-BdewSOMb.mjs.map → dist-CUEVu120.mjs.map} +1 -1
- package/dist/dist-DsdMtFME.mjs +3 -0
- package/dist/dist-Re6HHSqz.mjs +11561 -0
- package/dist/dist-Re6HHSqz.mjs.map +1 -0
- package/dist/index.mjs +132 -63
- package/dist/index.mjs.map +1 -1
- package/dist/{maybe-auto-update-BK4A8nTA.mjs → maybe-auto-update-q5MthdI8.mjs} +2 -2
- package/dist/{maybe-auto-update-BK4A8nTA.mjs.map → maybe-auto-update-q5MthdI8.mjs.map} +1 -1
- package/dist/skills-bundle/_AGENTS.mcp.md +101 -38
- package/dist/skills-bundle/_AGENTS.md +128 -49
- package/dist/{version-CUTRAMa8.mjs → version-DcR3O1UD.mjs} +2 -2
- package/dist/{version-CUTRAMa8.mjs.map → version-DcR3O1UD.mjs.map} +1 -1
- package/package.json +3 -3
- package/dist/dist-Bwn5tAiT.mjs.map +0 -1
- package/dist/dist-D8wvycYm.mjs +0 -127
- package/dist/dist-D8wvycYm.mjs.map +0 -1
- package/dist/dist-Dms4EW-W.mjs +0 -3
- package/dist/skills-bundle/skills/keystroke-actions/SKILL.md +0 -160
- package/dist/skills-bundle/skills/keystroke-actions/references/catalog-and-imports.md +0 -71
- package/dist/skills-bundle/skills/keystroke-agents/SKILL.md +0 -115
- package/dist/skills-bundle/skills/keystroke-agents/references/models.md +0 -23
- package/dist/skills-bundle/skills/keystroke-agents/references/tools-mcp-codemode.md +0 -73
- package/dist/skills-bundle/skills/keystroke-agents/references/workflows-and-testing.md +0 -26
- package/dist/skills-bundle/skills/keystroke-apps/SKILL.md +0 -151
- package/dist/skills-bundle/skills/keystroke-apps/references/cli-and-catalog.md +0 -104
- package/dist/skills-bundle/skills/keystroke-channels/SKILL.md +0 -66
- package/dist/skills-bundle/skills/keystroke-channels/references/slack-setup.md +0 -41
- package/dist/skills-bundle/skills/keystroke-cli/SKILL.md +0 -93
- package/dist/skills-bundle/skills/keystroke-deploy/SKILL.md +0 -93
- package/dist/skills-bundle/skills/keystroke-deploy/references/build-and-full-deploy.md +0 -30
- package/dist/skills-bundle/skills/keystroke-deploy/references/filtered-deploy.md +0 -50
- package/dist/skills-bundle/skills/keystroke-deploy/references/wip-ignore.md +0 -35
- package/dist/skills-bundle/skills/keystroke-files/SKILL.md +0 -43
- package/dist/skills-bundle/skills/keystroke-skills/SKILL.md +0 -42
- package/dist/skills-bundle/skills/keystroke-triggers/SKILL.md +0 -143
- package/dist/skills-bundle/skills/keystroke-workflows/SKILL.md +0 -78
- package/dist/skills-bundle/skills/keystroke-workflows/references/authoring.md +0 -168
- package/dist/skills-bundle/skills/keystroke-workflows/references/testing.md +0 -138
package/dist/dist-D8wvycYm.mjs
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { qn as parseStoredRouteManifest, qt as ROUTE_MANIFEST_REL_PATH } from "./dist-UdbUy9DM.mjs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import "@aws-sdk/client-s3";
|
|
8
|
-
import "@aws-sdk/s3-request-presigner";
|
|
9
|
-
import "node:crypto";
|
|
10
|
-
//#region ../../packages/storage/dist/pack-artifact-DVnIKrsg.mjs
|
|
11
|
-
/**
|
|
12
|
-
* Pack a directory tree that contains a `dist/` folder into a gzip tarball
|
|
13
|
-
* suitable for project-server extraction.
|
|
14
|
-
*/
|
|
15
|
-
function packDistTree(rootContainingDist) {
|
|
16
|
-
const tempDir = mkdtempSync(join(tmpdir(), "keystroke-artifact-pack-"));
|
|
17
|
-
const archivePath = join(tempDir, "artifact.tgz");
|
|
18
|
-
try {
|
|
19
|
-
const result = spawnSync("tar", [
|
|
20
|
-
"-czf",
|
|
21
|
-
archivePath,
|
|
22
|
-
"--exclude=._*",
|
|
23
|
-
"--exclude=.DS_Store",
|
|
24
|
-
"-C",
|
|
25
|
-
rootContainingDist,
|
|
26
|
-
"dist"
|
|
27
|
-
], {
|
|
28
|
-
encoding: "utf8",
|
|
29
|
-
env: {
|
|
30
|
-
...process.env,
|
|
31
|
-
COPYFILE_DISABLE: "1"
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
if (result.status !== 0) throw new Error(result.stderr?.trim() || "Failed to pack project artifact");
|
|
35
|
-
return readFileSync(archivePath);
|
|
36
|
-
} finally {
|
|
37
|
-
rmSync(tempDir, {
|
|
38
|
-
recursive: true,
|
|
39
|
-
force: true
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/** Extract a packed project artifact tarball into `destDir` (creates `destDir/dist/`). */
|
|
44
|
-
function extractProjectArtifact(archive, destDir) {
|
|
45
|
-
const tempDir = mkdtempSync(join(tmpdir(), "keystroke-artifact-extract-"));
|
|
46
|
-
const archivePath = join(tempDir, "artifact.tgz");
|
|
47
|
-
try {
|
|
48
|
-
writeFileSync(archivePath, archive);
|
|
49
|
-
const result = spawnSync("tar", [
|
|
50
|
-
"-xzf",
|
|
51
|
-
archivePath,
|
|
52
|
-
"-C",
|
|
53
|
-
destDir
|
|
54
|
-
], {
|
|
55
|
-
encoding: "utf8",
|
|
56
|
-
env: {
|
|
57
|
-
...process.env,
|
|
58
|
-
COPYFILE_DISABLE: "1"
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
if (result.status !== 0) throw new Error(result.stderr?.trim() || "Failed to extract project artifact");
|
|
62
|
-
} finally {
|
|
63
|
-
rmSync(tempDir, {
|
|
64
|
-
recursive: true,
|
|
65
|
-
force: true
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function moduleFileOf(entry) {
|
|
70
|
-
return "moduleFile" in entry && typeof entry.moduleFile === "string" ? entry.moduleFile : void 0;
|
|
71
|
-
}
|
|
72
|
-
/** Replace manifest rows for rebuilt modules while keeping untouched routes and metadata. */
|
|
73
|
-
function mergeStoredRouteManifest(base, rebuiltEntries) {
|
|
74
|
-
const rebuiltModuleFiles = new Set(rebuiltEntries.map(moduleFileOf).filter((value) => Boolean(value)));
|
|
75
|
-
const keptEntries = base.entries.filter((entry) => {
|
|
76
|
-
const moduleFile = moduleFileOf(entry);
|
|
77
|
-
if (!moduleFile) return true;
|
|
78
|
-
return !rebuiltModuleFiles.has(moduleFile);
|
|
79
|
-
});
|
|
80
|
-
const filteredRebuilt = rebuiltEntries.filter((entry) => entry.kind !== "health");
|
|
81
|
-
return {
|
|
82
|
-
...base,
|
|
83
|
-
entries: [...keptEntries, ...filteredRebuilt]
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
async function mergeFilteredArtifact(input) {
|
|
87
|
-
const mergeRoot = mkdtempSync(join(tmpdir(), "keystroke-artifact-merge-"));
|
|
88
|
-
try {
|
|
89
|
-
extractProjectArtifact(input.baseArchive, mergeRoot);
|
|
90
|
-
const manifestPath = join(mergeRoot, ROUTE_MANIFEST_REL_PATH);
|
|
91
|
-
const mergedManifest = mergeStoredRouteManifest(parseStoredRouteManifest(JSON.parse(readFileSync(manifestPath, "utf8"))), input.filtered.manifestEntries);
|
|
92
|
-
writeFileSync(manifestPath, `${JSON.stringify(mergedManifest, null, 2)}\n`);
|
|
93
|
-
for (const file of input.filtered.files) {
|
|
94
|
-
const destination = join(mergeRoot, "dist", file.relativePath);
|
|
95
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
96
|
-
writeFileSync(destination, file.contents);
|
|
97
|
-
if (file.sourceMap) writeFileSync(`${destination}.map`, file.sourceMap);
|
|
98
|
-
}
|
|
99
|
-
return packDistTree(mergeRoot);
|
|
100
|
-
} finally {
|
|
101
|
-
rmSync(mergeRoot, {
|
|
102
|
-
recursive: true,
|
|
103
|
-
force: true
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/** Pack `dist/` into a gzip tarball suitable for `/app` extraction in the project server image. */
|
|
108
|
-
function packProjectArtifact(projectRoot) {
|
|
109
|
-
if (!existsSync(join(projectRoot, "dist"))) throw new Error("dist/ not found — run keystroke build first");
|
|
110
|
-
return packDistTree(projectRoot);
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region ../../packages/storage/dist/index.mjs
|
|
114
|
-
/** Run `fn` over `items` in parallel batches of `batchSize`. */
|
|
115
|
-
async function mapInParallelBatches(items, batchSize, fn) {
|
|
116
|
-
if (batchSize < 1) throw new Error("batchSize must be at least 1");
|
|
117
|
-
const results = [];
|
|
118
|
-
for (let index = 0; index < items.length; index += batchSize) {
|
|
119
|
-
const batch = items.slice(index, index + batchSize);
|
|
120
|
-
results.push(...await Promise.all(batch.map(fn)));
|
|
121
|
-
}
|
|
122
|
-
return results;
|
|
123
|
-
}
|
|
124
|
-
//#endregion
|
|
125
|
-
export { mergeFilteredArtifact as n, packProjectArtifact as r, mapInParallelBatches as t };
|
|
126
|
-
|
|
127
|
-
//# sourceMappingURL=dist-D8wvycYm.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dist-D8wvycYm.mjs","names":[],"sources":["../../../packages/storage/dist/pack-artifact-DVnIKrsg.mjs","../../../packages/storage/dist/index.mjs"],"sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { tmpdir } from \"node:os\";\nimport { ROUTE_MANIFEST_REL_PATH, parseStoredRouteManifest } from \"@keystrokehq/shared\";\n//#region src/pack-dir.ts\n/**\n* Pack a directory tree that contains a `dist/` folder into a gzip tarball\n* suitable for project-server extraction.\n*/\nfunction packDistTree(rootContainingDist) {\n\tconst tempDir = mkdtempSync(join(tmpdir(), \"keystroke-artifact-pack-\"));\n\tconst archivePath = join(tempDir, \"artifact.tgz\");\n\ttry {\n\t\tconst result = spawnSync(\"tar\", [\n\t\t\t\"-czf\",\n\t\t\tarchivePath,\n\t\t\t\"--exclude=._*\",\n\t\t\t\"--exclude=.DS_Store\",\n\t\t\t\"-C\",\n\t\t\trootContainingDist,\n\t\t\t\"dist\"\n\t\t], {\n\t\t\tencoding: \"utf8\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tCOPYFILE_DISABLE: \"1\"\n\t\t\t}\n\t\t});\n\t\tif (result.status !== 0) throw new Error(result.stderr?.trim() || \"Failed to pack project artifact\");\n\t\treturn readFileSync(archivePath);\n\t} finally {\n\t\trmSync(tempDir, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/extract-artifact.ts\n/** Extract a packed project artifact tarball into `destDir` (creates `destDir/dist/`). */\nfunction extractProjectArtifact(archive, destDir) {\n\tconst tempDir = mkdtempSync(join(tmpdir(), \"keystroke-artifact-extract-\"));\n\tconst archivePath = join(tempDir, \"artifact.tgz\");\n\ttry {\n\t\twriteFileSync(archivePath, archive);\n\t\tconst result = spawnSync(\"tar\", [\n\t\t\t\"-xzf\",\n\t\t\tarchivePath,\n\t\t\t\"-C\",\n\t\t\tdestDir\n\t\t], {\n\t\t\tencoding: \"utf8\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tCOPYFILE_DISABLE: \"1\"\n\t\t\t}\n\t\t});\n\t\tif (result.status !== 0) throw new Error(result.stderr?.trim() || \"Failed to extract project artifact\");\n\t} finally {\n\t\trmSync(tempDir, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/merge-route-manifest.ts\nfunction moduleFileOf(entry) {\n\treturn \"moduleFile\" in entry && typeof entry.moduleFile === \"string\" ? entry.moduleFile : void 0;\n}\n/** Replace manifest rows for rebuilt modules while keeping untouched routes and metadata. */\nfunction mergeStoredRouteManifest(base, rebuiltEntries) {\n\tconst rebuiltModuleFiles = new Set(rebuiltEntries.map(moduleFileOf).filter((value) => Boolean(value)));\n\tconst keptEntries = base.entries.filter((entry) => {\n\t\tconst moduleFile = moduleFileOf(entry);\n\t\tif (!moduleFile) return true;\n\t\treturn !rebuiltModuleFiles.has(moduleFile);\n\t});\n\tconst filteredRebuilt = rebuiltEntries.filter((entry) => entry.kind !== \"health\");\n\treturn {\n\t\t...base,\n\t\tentries: [...keptEntries, ...filteredRebuilt]\n\t};\n}\n//#endregion\n//#region src/merge-filtered-artifact.ts\nasync function mergeFilteredArtifact(input) {\n\tconst mergeRoot = mkdtempSync(join(tmpdir(), \"keystroke-artifact-merge-\"));\n\ttry {\n\t\textractProjectArtifact(input.baseArchive, mergeRoot);\n\t\tconst manifestPath = join(mergeRoot, ROUTE_MANIFEST_REL_PATH);\n\t\tconst mergedManifest = mergeStoredRouteManifest(parseStoredRouteManifest(JSON.parse(readFileSync(manifestPath, \"utf8\"))), input.filtered.manifestEntries);\n\t\twriteFileSync(manifestPath, `${JSON.stringify(mergedManifest, null, 2)}\\n`);\n\t\tfor (const file of input.filtered.files) {\n\t\t\tconst destination = join(mergeRoot, \"dist\", file.relativePath);\n\t\t\tmkdirSync(dirname(destination), { recursive: true });\n\t\t\twriteFileSync(destination, file.contents);\n\t\t\tif (file.sourceMap) writeFileSync(`${destination}.map`, file.sourceMap);\n\t\t}\n\t\treturn packDistTree(mergeRoot);\n\t} finally {\n\t\trmSync(mergeRoot, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/pack-artifact.ts\n/** Pack `dist/` into a gzip tarball suitable for `/app` extraction in the project server image. */\nfunction packProjectArtifact(projectRoot) {\n\tif (!existsSync(join(projectRoot, \"dist\"))) throw new Error(\"dist/ not found — run keystroke build first\");\n\treturn packDistTree(projectRoot);\n}\n//#endregion\nexport { packDistTree as a, extractProjectArtifact as i, mergeFilteredArtifact as n, mergeStoredRouteManifest as r, packProjectArtifact as t };\n\n//# sourceMappingURL=pack-artifact-DVnIKrsg.mjs.map","import { a as packDistTree, i as extractProjectArtifact, n as mergeFilteredArtifact, r as mergeStoredRouteManifest, t as packProjectArtifact } from \"./pack-artifact-DVnIKrsg.mjs\";\nimport { CreateBucketCommand, DeleteBucketCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutBucketAclCommand, PutBucketPolicyCommand, PutObjectCommand, S3Client } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { tmpdir } from \"node:os\";\nimport { ROUTE_MANIFEST_REL_PATH } from \"@keystrokehq/shared\";\nimport { createHash } from \"node:crypto\";\n//#region src/config.ts\nfunction readEnv$1(env, ...keys) {\n\tfor (const key of keys) {\n\t\tconst value = env[key]?.trim();\n\t\tif (value) return value;\n\t}\n}\nfunction requireEnv(env, keys) {\n\tconst candidates = Array.isArray(keys) ? keys : [keys];\n\tconst value = readEnv$1(env, ...candidates);\n\tif (!value) throw new Error(`${candidates[0]} is required`);\n\treturn value;\n}\n/** Admin S3 credentials for storage plugins (bucket is per-org, not from env). */\nfunction storageAdminConfigFromEnv(env = process.env) {\n\tconst accessKeyId = requireEnv(env, [\"STORAGE_ACCESS_KEY_ID\", \"AWS_ACCESS_KEY_ID\"]);\n\tconst secretAccessKey = requireEnv(env, [\"STORAGE_SECRET_ACCESS_KEY\", \"AWS_SECRET_ACCESS_KEY\"]);\n\tconst endpoint = requireEnv(env, [\n\t\t\"STORAGE_ENDPOINT\",\n\t\t\"AWS_ENDPOINT_URL_S3\",\n\t\t\"AWS_S3_ENDPOINT\"\n\t]);\n\treturn {\n\t\tregion: readEnv$1(env, \"STORAGE_REGION\", \"AWS_REGION\") ?? \"us-east-1\",\n\t\taccessKeyId,\n\t\tsecretAccessKey,\n\t\tendpoint,\n\t\tforcePathStyle: env.STORAGE_FORCE_PATH_STYLE === void 0 ? true : env.STORAGE_FORCE_PATH_STYLE === \"true\"\n\t};\n}\n/** Full client config including bucket — for integration tests and explicit bucket callers. */\nfunction storageConfigFromEnv(env = process.env) {\n\tconst bucket = requireEnv(env, [\"STORAGE_BUCKET\", \"BUCKET_NAME\"]);\n\treturn {\n\t\t...storageAdminConfigFromEnv(env),\n\t\tbucket\n\t};\n}\n/** Endpoint for presigned URLs fetched inside project-server containers (e.g. `http://minio:9000`). */\nfunction containerPresignEndpointFromEnv(hostEndpoint, env = process.env) {\n\tconst override = readEnv$1(env, \"STORAGE_CONTAINER_ENDPOINT\");\n\tif (override) return override;\n\ttry {\n\t\tconst url = new URL(hostEndpoint);\n\t\tif (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") {\n\t\t\tconst port = url.port || (url.protocol === \"https:\" ? \"443\" : \"9000\");\n\t\t\treturn `${url.protocol}//minio:${port}`;\n\t\t}\n\t} catch {}\n\treturn hostEndpoint;\n}\n//#endregion\n//#region src/keys.ts\n/** Object key for a built project artifact tarball. */\nfunction projectArtifactKey(projectId, version) {\n\treturn `projects/${projectId}/artifacts/${version}.tgz`;\n}\n/**\n* Per-artifact source manifest (the deploy's file tree: path -> id + content\n* hash, plus resource refs). One per deploy.\n*/\nfunction projectSourceManifestKey(projectId, artifactId) {\n\treturn `projects/${projectId}/sources/${artifactId}/index.json`;\n}\nconst SHA256_HEX = /^[a-f0-9]{64}$/;\n/** True when `hash` is a lowercase hex sha256 digest (64 chars). */\nfunction isSourceBlobHash(hash) {\n\treturn SHA256_HEX.test(hash);\n}\n/**\n* Content-addressed key for a single source file's contents, deduped across all\n* of a project's deploys. `hash` is a lowercase hex sha256 of the file bytes.\n*/\nfunction projectSourceBlobKey(projectId, hash) {\n\tif (!isSourceBlobHash(hash)) throw new Error(\"Invalid source blob hash\");\n\treturn `projects/${projectId}/sources/blobs/${hash}`;\n}\nconst AVATAR_EXTENSION = /^[a-z0-9]+$/;\n/** Object key for a chat attachment in org-private storage. */\nfunction chatAttachmentStorageKey(projectId, id, ext) {\n\tconst normalizedExt = ext.replace(/^\\./, \"\").toLowerCase();\n\tif (!AVATAR_EXTENSION.test(normalizedExt)) throw new Error(\"Invalid chat attachment extension\");\n\treturn `chat-attachments/${projectId}/${id}.${normalizedExt}`;\n}\n/** True when `key` is a chat attachment owned by `projectId`. */\nfunction isChatAttachmentStorageKey(key, projectId) {\n\tconst prefix = `chat-attachments/${projectId}/`;\n\tif (!key.startsWith(prefix)) return false;\n\tconst remainder = key.slice(prefix.length);\n\treturn remainder.length > 0 && !remainder.includes(\"/\");\n}\n/** Object key for a user's custom profile avatar in the shared keystroke-users bucket. */\nfunction userAvatarStorageKey(userId, extension) {\n\tconst ext = extension.replace(/^\\./, \"\").toLowerCase();\n\tif (!AVATAR_EXTENSION.test(ext)) throw new Error(\"Invalid avatar extension\");\n\treturn `avatars/${userId}/avatar.${ext}`;\n}\n/** True when `key` is the canonical avatar object for `userId`. */\nfunction isUserAvatarStorageKey(key, userId) {\n\tconst prefix = `avatars/${userId}/avatar.`;\n\tif (!key.startsWith(prefix)) return false;\n\tconst ext = key.slice(prefix.length);\n\treturn AVATAR_EXTENSION.test(ext);\n}\n/** Object key for an org logo in the shared keystroke-assets bucket. */\nfunction orgLogoStorageKey(organizationId, variant, extension) {\n\tconst ext = extension.replace(/^\\./, \"\").toLowerCase();\n\tif (!AVATAR_EXTENSION.test(ext)) throw new Error(\"Invalid org logo extension\");\n\treturn `org-logos/${organizationId}/logo-${variant}.${ext}`;\n}\n/** True when `key` is the canonical logo object for `organizationId` and `variant`. */\nfunction isOrgLogoStorageKey(key, organizationId, variant) {\n\tconst prefix = `org-logos/${organizationId}/logo-${variant}.`;\n\tif (!key.startsWith(prefix)) return false;\n\tconst ext = key.slice(prefix.length);\n\treturn AVATAR_EXTENSION.test(ext);\n}\n//#endregion\n//#region src/map-in-batches.ts\n/** Default concurrency for parallel blob downloads and file writes. */\nconst DEFAULT_PARALLEL_BATCH_SIZE = 8;\n/** Run `fn` over `items` in parallel batches of `batchSize`. */\nasync function mapInParallelBatches(items, batchSize, fn) {\n\tif (batchSize < 1) throw new Error(\"batchSize must be at least 1\");\n\tconst results = [];\n\tfor (let index = 0; index < items.length; index += batchSize) {\n\t\tconst batch = items.slice(index, index + batchSize);\n\t\tresults.push(...await Promise.all(batch.map(fn)));\n\t}\n\treturn results;\n}\n//#endregion\n//#region src/load-project-source-files.ts\n/** Matches deploy-time source snapshot limits in @keystrokehq/build walk-project. */\nconst MAX_ACTIVE_SOURCE_DOWNLOAD_BYTES = 8 * 1024 * 1024;\nasync function loadProjectSourceFiles(storage, projectId, manifest, options = {}) {\n\tconst maxTotalBytes = options.maxTotalBytes ?? 8388608;\n\tconst files = [];\n\tlet totalBytes = 0;\n\tfor (let index = 0; index < manifest.files.length; index += 8) {\n\t\tconst batch = manifest.files.slice(index, index + 8);\n\t\tconst batchResults = await Promise.all(batch.map(async (file) => {\n\t\t\tconst bytes = await storage.getObject(projectSourceBlobKey(projectId, file.hash));\n\t\t\tconst contents = new TextDecoder().decode(bytes);\n\t\t\treturn {\n\t\t\t\tpath: file.path,\n\t\t\t\tcontents,\n\t\t\t\tbyteLength: Buffer.byteLength(contents, \"utf8\")\n\t\t\t};\n\t\t}));\n\t\tfor (const result of batchResults) {\n\t\t\ttotalBytes += result.byteLength;\n\t\t\tif (totalBytes > maxTotalBytes) throw new Error(\"Active source snapshot exceeds download size limit\");\n\t\t\tfiles.push({\n\t\t\t\tpath: result.path,\n\t\t\t\tcontents: result.contents\n\t\t\t});\n\t\t}\n\t}\n\treturn files;\n}\n//#endregion\n//#region src/create-storage.ts\nconst DEFAULT_PRESIGN_TTL_SECONDS = 900;\nfunction createStorage(config) {\n\tconst client = new S3Client(toClientConfig(config));\n\treturn {\n\t\tasync putObject(input) {\n\t\t\tawait client.send(new PutObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: input.key,\n\t\t\t\tBody: input.body,\n\t\t\t\tContentType: input.contentType\n\t\t\t}));\n\t\t},\n\t\tasync getObject(key) {\n\t\t\tconst response = await client.send(new GetObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: key\n\t\t\t}));\n\t\t\tif (!response.Body) throw new Error(`Object body missing for key ${key}`);\n\t\t\treturn response.Body.transformToByteArray();\n\t\t},\n\t\tasync headObject(key) {\n\t\t\ttry {\n\t\t\t\treturn { contentLength: (await client.send(new HeadObjectCommand({\n\t\t\t\t\tBucket: config.bucket,\n\t\t\t\t\tKey: key\n\t\t\t\t}))).ContentLength ?? 0 };\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tasync presignGet(input) {\n\t\t\treturn getSignedUrl(client, new GetObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: input.key\n\t\t\t}), { expiresIn: input.expiresInSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS });\n\t\t},\n\t\tasync presignPut(input) {\n\t\t\treturn getSignedUrl(client, new PutObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: input.key,\n\t\t\t\tContentType: input.contentType ?? \"application/gzip\"\n\t\t\t}), { expiresIn: input.expiresInSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS });\n\t\t},\n\t\tasync deleteObject(key) {\n\t\t\tawait client.send(new DeleteObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: key\n\t\t\t}));\n\t\t},\n\t\tasync listObjects(prefix) {\n\t\t\tconst keys = [];\n\t\t\tlet continuationToken;\n\t\t\tdo {\n\t\t\t\tconst response = await client.send(new ListObjectsV2Command({\n\t\t\t\t\tBucket: config.bucket,\n\t\t\t\t\tPrefix: prefix,\n\t\t\t\t\tContinuationToken: continuationToken\n\t\t\t\t}));\n\t\t\t\tfor (const object of response.Contents ?? []) if (object.Key) keys.push(object.Key);\n\t\t\t\tcontinuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;\n\t\t\t} while (continuationToken);\n\t\t\treturn keys;\n\t\t}\n\t};\n}\n/** Presign URLs for project-server containers using `STORAGE_CONTAINER_ENDPOINT` when set. */\nfunction createContainerPresignStorage(config, env = process.env) {\n\treturn createStorage({\n\t\t...config,\n\t\tendpoint: containerPresignEndpointFromEnv(config.endpoint, env)\n\t});\n}\nfunction toClientConfig(config) {\n\treturn {\n\t\tregion: config.region,\n\t\tcredentials: {\n\t\t\taccessKeyId: config.accessKeyId,\n\t\t\tsecretAccessKey: config.secretAccessKey\n\t\t},\n\t\tendpoint: config.endpoint,\n\t\tforcePathStyle: config.forcePathStyle\n\t};\n}\nfunction isNotFoundError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst name = \"name\" in error ? String(error.name) : \"\";\n\tconst status = \"$metadata\" in error ? error.$metadata : void 0;\n\treturn name === \"NotFound\" || name === \"NoSuchKey\" || status?.httpStatusCode === 404;\n}\n//#endregion\n//#region src/extract-route-manifest.ts\nvar RouteManifestNotFoundError = class extends Error {\n\tconstructor(message = `Route manifest missing in artifact (${ROUTE_MANIFEST_REL_PATH})`) {\n\t\tsuper(message);\n\t\tthis.name = \"RouteManifestNotFoundError\";\n\t}\n};\n/** Read `.keystroke/route-manifest.json` from a packed project artifact tarball. */\nfunction extractRouteManifestFromArtifact(archive) {\n\tconst tempDir = mkdtempSync(join(tmpdir(), \"keystroke-artifact-\"));\n\tconst archivePath = join(tempDir, \"artifact.tgz\");\n\ttry {\n\t\twriteFileSync(archivePath, archive);\n\t\tif (spawnSync(\"tar\", [\n\t\t\t\"-xzf\",\n\t\t\tarchivePath,\n\t\t\t\"-C\",\n\t\t\ttempDir,\n\t\t\tROUTE_MANIFEST_REL_PATH\n\t\t], { encoding: \"utf8\" }).status !== 0) throw new RouteManifestNotFoundError();\n\t\tconst raw = readFileSync(join(tempDir, ROUTE_MANIFEST_REL_PATH), \"utf8\");\n\t\treturn JSON.parse(raw);\n\t} catch (error) {\n\t\tif (error instanceof RouteManifestNotFoundError) throw error;\n\t\tthrow new RouteManifestNotFoundError(error instanceof Error ? error.message : \"Failed to extract route manifest\");\n\t} finally {\n\t\trmSync(tempDir, {\n\t\t\trecursive: true,\n\t\t\tforce: true\n\t\t});\n\t}\n}\n//#endregion\n//#region src/s3-errors.ts\nfunction isBucketAlreadyExistsError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst name = \"name\" in error ? String(error.name) : \"\";\n\tconst code = \"Code\" in error ? String(error.Code) : \"\";\n\treturn name === \"BucketAlreadyOwnedByYou\" || code === \"BucketAlreadyOwnedByYou\" || name === \"BucketAlreadyExists\" || code === \"BucketAlreadyExists\";\n}\nfunction isStorageNotFoundError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst record = error;\n\tconst name = typeof record.name === \"string\" ? record.name : \"\";\n\tconst code = typeof record.Code === \"string\" ? record.Code : \"\";\n\tconst status = typeof record.$metadata === \"object\" && record.$metadata !== null ? record.$metadata.httpStatusCode : void 0;\n\treturn name === \"NoSuchKey\" || code === \"NoSuchKey\" || code === \"NotFound\" || status === 404;\n}\nfunction isStorageBucketNotFoundError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst record = error;\n\tconst name = typeof record.name === \"string\" ? record.name : \"\";\n\tconst code = typeof record.Code === \"string\" ? record.Code : \"\";\n\tconst status = typeof record.$metadata === \"object\" && record.$metadata !== null ? record.$metadata.httpStatusCode : void 0;\n\treturn name === \"NoSuchBucket\" || code === \"NoSuchBucket\" || name === \"NotFound\" || status === 404;\n}\n//#endregion\n//#region src/asset-storage.ts\n/** Shared bucket for cross-org public assets (avatars, org sidebar logos). */\nconst KEYSTROKE_ASSETS_BUCKET = \"keystroke-assets\";\n/** Env var for the browser-facing origin of public asset objects (no trailing slash). */\nconst KEYSTROKE_ASSETS_PUBLIC_URL_BASE_ENV = \"KEYSTROKE_ASSETS_PUBLIC_URL_BASE\";\nconst KEYSTROKE_ASSETS_AVATAR_PREFIX = \"avatars/\";\nconst KEYSTROKE_ASSETS_ORG_LOGO_PREFIX = \"org-logos/\";\nfunction readEnv(env, key) {\n\treturn env[key]?.trim() || void 0;\n}\n/** Default path-style base when `KEYSTROKE_ASSETS_PUBLIC_URL_BASE` is unset (local MinIO). */\nfunction defaultKeystrokeAssetsPublicUrlBase(env = process.env) {\n\treturn `${storageAdminConfigFromEnv(env).endpoint.replace(/\\/+$/, \"\")}/${KEYSTROKE_ASSETS_BUCKET}`;\n}\n/** Public URL prefix for asset objects — configured base or local path-style default. */\nfunction keystrokeAssetsPublicUrlBase(env = process.env) {\n\tconst configured = readEnv(env, KEYSTROKE_ASSETS_PUBLIC_URL_BASE_ENV);\n\tif (configured) return configured.replace(/\\/+$/, \"\");\n\treturn defaultKeystrokeAssetsPublicUrlBase(env);\n}\nfunction keystrokeAssetsPublicUrlPrefixes(env = process.env) {\n\tconst prefixes = [keystrokeAssetsPublicUrlBase(env)];\n\tif (readEnv(env, \"KEYSTROKE_ASSETS_PUBLIC_URL_BASE\")) prefixes.push(defaultKeystrokeAssetsPublicUrlBase(env));\n\treturn [...new Set(prefixes)];\n}\nfunction createAssetsStorageClient(env = process.env) {\n\treturn createStorage({\n\t\t...storageAdminConfigFromEnv(env),\n\t\tbucket: KEYSTROKE_ASSETS_BUCKET\n\t});\n}\n/** Stable public URL for an object in the keystroke-assets bucket. */\nfunction publicKeystrokeAssetsObjectUrl(key, env = process.env) {\n\treturn `${keystrokeAssetsPublicUrlBase(env)}/${key}`;\n}\n/** Extract the object key from a public keystroke-assets URL when it matches `requiredPrefix`. */\nfunction keystrokeAssetsKeyFromPublicUrl(url, requiredPrefix, env = process.env) {\n\tfor (const prefix of keystrokeAssetsPublicUrlPrefixes(env)) {\n\t\tconst normalizedPrefix = `${prefix.replace(/\\/+$/, \"\")}/`;\n\t\tif (!url.startsWith(normalizedPrefix)) continue;\n\t\tconst key = decodeURIComponent(url.slice(normalizedPrefix.length).split(\"?\")[0] ?? \"\");\n\t\tif (key.startsWith(requiredPrefix)) return key;\n\t}\n\treturn null;\n}\nfunction isKeystrokeAssetsAvatarPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_AVATAR_PREFIX, env) !== null;\n}\nfunction keystrokeAssetsAvatarKeyFromPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_AVATAR_PREFIX, env);\n}\nfunction isKeystrokeAssetsOrgLogoPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_ORG_LOGO_PREFIX, env) !== null;\n}\nfunction keystrokeAssetsOrgLogoKeyFromPublicUrl(url, env = process.env) {\n\treturn keystrokeAssetsKeyFromPublicUrl(url, KEYSTROKE_ASSETS_ORG_LOGO_PREFIX, env);\n}\nasync function ensureKeystrokeAssetsBucket(env = process.env) {\n\tconst adminConfig = storageAdminConfigFromEnv(env);\n\tconst adminClient = new S3Client({\n\t\tregion: adminConfig.region,\n\t\tcredentials: {\n\t\t\taccessKeyId: adminConfig.accessKeyId,\n\t\t\tsecretAccessKey: adminConfig.secretAccessKey\n\t\t},\n\t\tendpoint: adminConfig.endpoint,\n\t\tforcePathStyle: adminConfig.forcePathStyle\n\t});\n\tawait createAssetsBucketIfNeeded(adminClient, KEYSTROKE_ASSETS_BUCKET);\n\tawait applyPublicAssetsBucketAcl(adminClient, KEYSTROKE_ASSETS_BUCKET);\n\tawait applyPublicAssetsBucketPolicy(adminClient, KEYSTROKE_ASSETS_BUCKET);\n}\nasync function createAssetsBucketIfNeeded(adminClient, bucket) {\n\ttry {\n\t\tawait adminClient.send(new CreateBucketCommand({\n\t\t\tBucket: bucket,\n\t\t\tACL: \"public-read\"\n\t\t}));\n\t\treturn;\n\t} catch (error) {\n\t\tif (isBucketAlreadyExistsError(error)) return;\n\t}\n\ttry {\n\t\tawait adminClient.send(new CreateBucketCommand({ Bucket: bucket }));\n\t} catch (error) {\n\t\tif (!isBucketAlreadyExistsError(error)) throw error;\n\t}\n}\nasync function applyPublicAssetsBucketAcl(adminClient, bucket) {\n\ttry {\n\t\tawait adminClient.send(new PutBucketAclCommand({\n\t\t\tBucket: bucket,\n\t\t\tACL: \"public-read\"\n\t\t}));\n\t} catch (error) {\n\t\tif (!isUnsupportedStorageFeatureError(error)) console.warn(\"[storage] PutBucketAcl failed (continuing):\", error);\n\t}\n}\nasync function applyPublicAssetsBucketPolicy(adminClient, bucket) {\n\ttry {\n\t\tawait adminClient.send(new PutBucketPolicyCommand({\n\t\t\tBucket: bucket,\n\t\t\tPolicy: JSON.stringify({\n\t\t\t\tVersion: \"2012-10-17\",\n\t\t\t\tStatement: [{\n\t\t\t\t\tEffect: \"Allow\",\n\t\t\t\t\tPrincipal: \"*\",\n\t\t\t\t\tAction: [\"s3:GetObject\"],\n\t\t\t\t\tResource: [`arn:aws:s3:::${bucket}/${KEYSTROKE_ASSETS_AVATAR_PREFIX}*`, `arn:aws:s3:::${bucket}/${KEYSTROKE_ASSETS_ORG_LOGO_PREFIX}*`]\n\t\t\t\t}]\n\t\t\t})\n\t\t}));\n\t} catch (error) {\n\t\tif (!isUnsupportedStorageFeatureError(error)) console.warn(\"[storage] PutBucketPolicy failed (continuing):\", error);\n\t}\n}\nfunction isUnsupportedStorageFeatureError(error) {\n\tif (!error || typeof error !== \"object\") return false;\n\tconst name = \"name\" in error ? String(error.name) : \"\";\n\tconst code = \"Code\" in error ? String(error.Code) : \"\";\n\treturn name === \"NotImplemented\" || code === \"NotImplemented\" || code === \"NotSupported\";\n}\n//#endregion\n//#region src/provision/names.ts\nconst DEFAULT_BUCKET_NAME_PREFIX = \"ks\";\nconst S3_BUCKET_NAME_MAX = 63;\n/** Deterministic bucket segment; keeps short test ids readable, hashes UUID-length ids. */\nfunction bucketIdSegment(id, hashLength = 10) {\n\tif (id.length <= 24 && !/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(id)) return id;\n\treturn createHash(\"sha256\").update(id).digest(\"hex\").slice(0, hashLength);\n}\nfunction assertBucketName(name) {\n\tif (name.length > S3_BUCKET_NAME_MAX) throw new Error(`bucket name exceeds ${S3_BUCKET_NAME_MAX} chars (${name.length}): ${name}`);\n\treturn name;\n}\nfunction orgStorageBucketName(input, prefix = DEFAULT_BUCKET_NAME_PREFIX) {\n\treturn assertBucketName(`${prefix.trim().replace(/-+$/g, \"\")}-${input.organizationId.toLowerCase()}`);\n}\nfunction orgAgentBucketName(input) {\n\tconst orgBucket = input.orgBucket ?? (input.organizationId ? orgStorageBucketName({ organizationId: input.organizationId }) : void 0);\n\tif (!orgBucket) throw new Error(\"orgAgentBucketName requires organizationId or orgBucket\");\n\treturn assertBucketName(`${orgBucket}-${bucketIdSegment(input.agentId)}`);\n}\n//#endregion\n//#region src/workspace/sync.ts\nconst DEFAULT_UPLOAD_CONCURRENCY = 8;\nfunction walkFiles(root) {\n\tconst files = [];\n\tconst stack = [root];\n\twhile (stack.length > 0) {\n\t\tconst current = stack.pop();\n\t\tfor (const entry of readdirSync(current, { withFileTypes: true })) {\n\t\t\tconst full = join(current, entry.name);\n\t\t\tif (entry.isDirectory()) stack.push(full);\n\t\t\telse if (entry.isFile()) files.push(full);\n\t\t}\n\t}\n\treturn files;\n}\nfunction collectWorkspaceObjects(workspaceRoot) {\n\treturn walkFiles(workspaceRoot).map((file) => ({\n\t\tkey: relative(workspaceRoot, file).split(\"\\\\\").join(\"/\"),\n\t\tbody: readFileSync(file)\n\t}));\n}\n/** Keys present in object storage but absent from the local workspace root. */\nfunction workspaceKeysToDelete(remoteKeys, localKeys) {\n\treturn remoteKeys.filter((key) => !localKeys.has(key));\n}\nfunction s3Client(config) {\n\treturn new S3Client({\n\t\tregion: config.region,\n\t\tendpoint: config.endpoint,\n\t\tforcePathStyle: config.forcePathStyle,\n\t\tcredentials: {\n\t\t\taccessKeyId: config.accessKeyId,\n\t\t\tsecretAccessKey: config.secretAccessKey\n\t\t}\n\t});\n}\n/** Create the bucket named in `config` when missing (idempotent for races). */\nasync function ensureStorageBucket(config) {\n\tconst client = s3Client(config);\n\ttry {\n\t\tawait client.send(new CreateBucketCommand({ Bucket: config.bucket }));\n\t} catch (error) {\n\t\tif (!isBucketAlreadyExistsError(error)) throw error;\n\t}\n}\nasync function uploadWorkspaceObjects(config, objects, concurrency = DEFAULT_UPLOAD_CONCURRENCY) {\n\tif (objects.length === 0) return;\n\tconst client = s3Client(config);\n\tlet index = 0;\n\tasync function worker() {\n\t\twhile (index < objects.length) {\n\t\t\tconst current = objects[index++];\n\t\t\tawait client.send(new PutObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: current.key,\n\t\t\t\tBody: current.body\n\t\t\t}));\n\t\t}\n\t}\n\tconst workers = Array.from({ length: Math.min(concurrency, objects.length) }, () => worker());\n\tawait Promise.all(workers);\n}\nasync function deleteWorkspaceObjects(config, keys, concurrency = DEFAULT_UPLOAD_CONCURRENCY) {\n\tif (keys.length === 0) return;\n\tconst client = s3Client(config);\n\tlet index = 0;\n\tasync function worker() {\n\t\twhile (index < keys.length) {\n\t\t\tconst key = keys[index++];\n\t\t\tawait client.send(new DeleteObjectCommand({\n\t\t\t\tBucket: config.bucket,\n\t\t\t\tKey: key\n\t\t\t}));\n\t\t}\n\t}\n\tconst workers = Array.from({ length: Math.min(concurrency, keys.length) }, () => worker());\n\tawait Promise.all(workers);\n}\n/** Mirror the host agent workspace directory to the per-agent bucket (upsert + delete stale keys). */\nasync function syncAgentWorkspaceToBucket(input) {\n\tconst bucket = orgAgentBucketName({\n\t\torgBucket: input.orgBucket,\n\t\tagentId: input.agentId\n\t});\n\tconst config = {\n\t\t...input.admin,\n\t\tbucket\n\t};\n\tawait ensureStorageBucket(config);\n\tconst objects = collectWorkspaceObjects(input.agentRoot);\n\tconst localKeys = new Set(objects.map((object) => object.key));\n\tawait deleteWorkspaceObjects(config, workspaceKeysToDelete(await createStorage(config).listObjects(\"\"), localKeys));\n\tawait uploadWorkspaceObjects(config, objects);\n}\n//#endregion\n//#region src/provision/minio.ts\nfunction minioStoragePlugin(options = {}) {\n\tconst adminConfig = storageAdminConfigFromEnv(options.env ?? process.env);\n\tconst adminClient = new S3Client({\n\t\tregion: adminConfig.region,\n\t\tcredentials: {\n\t\t\taccessKeyId: adminConfig.accessKeyId,\n\t\t\tsecretAccessKey: adminConfig.secretAccessKey\n\t\t},\n\t\tendpoint: adminConfig.endpoint,\n\t\tforcePathStyle: adminConfig.forcePathStyle\n\t});\n\treturn {\n\t\tasync provisionOrganization(input) {\n\t\t\tconst bucket = orgStorageBucketName(input, options.bucketNamePrefix);\n\t\t\ttry {\n\t\t\t\tawait adminClient.send(new CreateBucketCommand({ Bucket: bucket }));\n\t\t\t} catch (error) {\n\t\t\t\tif (!isBucketAlreadyExistsError(error)) throw error;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tbucket,\n\t\t\t\tendpoint: adminConfig.endpoint,\n\t\t\t\taccessKeyId: adminConfig.accessKeyId,\n\t\t\t\tsecretAccessKey: adminConfig.secretAccessKey,\n\t\t\t\tregion: adminConfig.region,\n\t\t\t\tforcePathStyle: adminConfig.forcePathStyle\n\t\t\t};\n\t\t},\n\t\tasync deprovisionOrganization(result) {\n\t\t\tconst client = new S3Client({\n\t\t\t\tregion: result.region ?? adminConfig.region,\n\t\t\t\tcredentials: {\n\t\t\t\t\taccessKeyId: result.accessKeyId,\n\t\t\t\t\tsecretAccessKey: result.secretAccessKey\n\t\t\t\t},\n\t\t\t\tendpoint: result.endpoint,\n\t\t\t\tforcePathStyle: result.forcePathStyle ?? true\n\t\t\t});\n\t\t\tif (((await client.send(new ListObjectsV2Command({\n\t\t\t\tBucket: result.bucket,\n\t\t\t\tMaxKeys: 1\n\t\t\t}))).KeyCount ?? 0) > 0) throw new Error(`Refusing to delete non-empty storage bucket ${result.bucket}`);\n\t\t\tawait client.send(new DeleteBucketCommand({ Bucket: result.bucket }));\n\t\t}\n\t};\n}\n//#endregion\nexport { DEFAULT_PARALLEL_BATCH_SIZE, KEYSTROKE_ASSETS_AVATAR_PREFIX, KEYSTROKE_ASSETS_BUCKET, KEYSTROKE_ASSETS_ORG_LOGO_PREFIX, KEYSTROKE_ASSETS_PUBLIC_URL_BASE_ENV, MAX_ACTIVE_SOURCE_DOWNLOAD_BYTES, RouteManifestNotFoundError, bucketIdSegment, chatAttachmentStorageKey, collectWorkspaceObjects, containerPresignEndpointFromEnv, createAssetsStorageClient, createContainerPresignStorage, createStorage, defaultKeystrokeAssetsPublicUrlBase, deleteWorkspaceObjects, ensureKeystrokeAssetsBucket, ensureStorageBucket, extractProjectArtifact, extractRouteManifestFromArtifact, isBucketAlreadyExistsError, isChatAttachmentStorageKey, isKeystrokeAssetsAvatarPublicUrl, isKeystrokeAssetsOrgLogoPublicUrl, isOrgLogoStorageKey, isSourceBlobHash, isStorageBucketNotFoundError, isStorageNotFoundError, isUserAvatarStorageKey, keystrokeAssetsAvatarKeyFromPublicUrl, keystrokeAssetsKeyFromPublicUrl, keystrokeAssetsOrgLogoKeyFromPublicUrl, keystrokeAssetsPublicUrlBase, loadProjectSourceFiles, mapInParallelBatches, mergeFilteredArtifact, mergeStoredRouteManifest, minioStoragePlugin, orgAgentBucketName, orgLogoStorageKey, orgStorageBucketName, packDistTree, packProjectArtifact, projectArtifactKey, projectSourceBlobKey, projectSourceManifestKey, publicKeystrokeAssetsObjectUrl, storageAdminConfigFromEnv, storageConfigFromEnv, syncAgentWorkspaceToBucket, uploadWorkspaceObjects, userAvatarStorageKey, workspaceKeysToDelete };\n\n//# sourceMappingURL=index.mjs.map"],"mappings":";;;;;;;;;;;;;;AAUA,SAAS,aAAa,oBAAoB;CACzC,MAAM,UAAU,YAAY,KAAK,OAAO,GAAG,0BAA0B,CAAC;CACtE,MAAM,cAAc,KAAK,SAAS,cAAc;CAChD,IAAI;EACH,MAAM,SAAS,UAAU,OAAO;GAC/B;GACA;GACA;GACA;GACA;GACA;GACA;EACD,GAAG;GACF,UAAU;GACV,KAAK;IACJ,GAAG,QAAQ;IACX,kBAAkB;GACnB;EACD,CAAC;EACD,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,MAAM,OAAO,QAAQ,KAAK,KAAK,iCAAiC;EACnG,OAAO,aAAa,WAAW;CAChC,UAAU;EACT,OAAO,SAAS;GACf,WAAW;GACX,OAAO;EACR,CAAC;CACF;AACD;;AAIA,SAAS,uBAAuB,SAAS,SAAS;CACjD,MAAM,UAAU,YAAY,KAAK,OAAO,GAAG,6BAA6B,CAAC;CACzE,MAAM,cAAc,KAAK,SAAS,cAAc;CAChD,IAAI;EACH,cAAc,aAAa,OAAO;EAClC,MAAM,SAAS,UAAU,OAAO;GAC/B;GACA;GACA;GACA;EACD,GAAG;GACF,UAAU;GACV,KAAK;IACJ,GAAG,QAAQ;IACX,kBAAkB;GACnB;EACD,CAAC;EACD,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,MAAM,OAAO,QAAQ,KAAK,KAAK,oCAAoC;CACvG,UAAU;EACT,OAAO,SAAS;GACf,WAAW;GACX,OAAO;EACR,CAAC;CACF;AACD;AAGA,SAAS,aAAa,OAAO;CAC5B,OAAO,gBAAgB,SAAS,OAAO,MAAM,eAAe,WAAW,MAAM,aAAa,KAAK;AAChG;;AAEA,SAAS,yBAAyB,MAAM,gBAAgB;CACvD,MAAM,qBAAqB,IAAI,IAAI,eAAe,IAAI,YAAY,EAAE,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC;CACrG,MAAM,cAAc,KAAK,QAAQ,QAAQ,UAAU;EAClD,MAAM,aAAa,aAAa,KAAK;EACrC,IAAI,CAAC,YAAY,OAAO;EACxB,OAAO,CAAC,mBAAmB,IAAI,UAAU;CAC1C,CAAC;CACD,MAAM,kBAAkB,eAAe,QAAQ,UAAU,MAAM,SAAS,QAAQ;CAChF,OAAO;EACN,GAAG;EACH,SAAS,CAAC,GAAG,aAAa,GAAG,eAAe;CAC7C;AACD;AAGA,eAAe,sBAAsB,OAAO;CAC3C,MAAM,YAAY,YAAY,KAAK,OAAO,GAAG,2BAA2B,CAAC;CACzE,IAAI;EACH,uBAAuB,MAAM,aAAa,SAAS;EACnD,MAAM,eAAe,KAAK,WAAW,uBAAuB;EAC5D,MAAM,iBAAiB,yBAAyB,yBAAyB,KAAK,MAAM,aAAa,cAAc,MAAM,CAAC,CAAC,GAAG,MAAM,SAAS,eAAe;EACxJ,cAAc,cAAc,GAAG,KAAK,UAAU,gBAAgB,MAAM,CAAC,EAAE,GAAG;EAC1E,KAAK,MAAM,QAAQ,MAAM,SAAS,OAAO;GACxC,MAAM,cAAc,KAAK,WAAW,QAAQ,KAAK,YAAY;GAC7D,UAAU,QAAQ,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;GACnD,cAAc,aAAa,KAAK,QAAQ;GACxC,IAAI,KAAK,WAAW,cAAc,GAAG,YAAY,OAAO,KAAK,SAAS;EACvE;EACA,OAAO,aAAa,SAAS;CAC9B,UAAU;EACT,OAAO,WAAW;GACjB,WAAW;GACX,OAAO;EACR,CAAC;CACF;AACD;;AAIA,SAAS,oBAAoB,aAAa;CACzC,IAAI,CAAC,WAAW,KAAK,aAAa,MAAM,CAAC,GAAG,MAAM,IAAI,MAAM,6CAA6C;CACzG,OAAO,aAAa,WAAW;AAChC;;;;ACiBA,eAAe,qBAAqB,OAAO,WAAW,IAAI;CACzD,IAAI,YAAY,GAAG,MAAM,IAAI,MAAM,8BAA8B;CACjE,MAAM,UAAU,CAAC;CACjB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,WAAW;EAC7D,MAAM,QAAQ,MAAM,MAAM,OAAO,QAAQ,SAAS;EAClD,QAAQ,KAAK,GAAG,MAAM,QAAQ,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC;CACjD;CACA,OAAO;AACR"}
|
package/dist/dist-Dms4EW-W.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: keystroke-actions
|
|
3
|
-
description: Define keystroke actions — workflow steps and agent tools. Custom API actions use a credential (defineCredential) or an app wrapper (app.action); catalog integrations import from npm packages. Use when authoring src/actions/ or wiring integrations.
|
|
4
|
-
metadata:
|
|
5
|
-
keystroke-domain: actions
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Actions
|
|
9
|
-
|
|
10
|
-
One **executable unit** used everywhere: workflow steps, agent tools, and codemode host calls.
|
|
11
|
-
|
|
12
|
-
Anything that calls an external API needs a **credential**. Two paths: declare a standalone `defineCredential` on a `defineAction` (simplest), or use an **app** wrapper (`app.action()`) when several actions share one connection or you've synced a catalog/custom app into `src/apps/`. See [apps skill](.agents/skills/keystroke-apps/SKILL.md).
|
|
13
|
-
|
|
14
|
-
## Actions are leaf units — never call an action from an action
|
|
15
|
-
|
|
16
|
-
An action's `run` must **not** call another action (yours or an integration action like `slackSendMessage`). Composition belongs in a **workflow**; an integration action is used directly as a workflow step or agent tool — not wrapped in a custom action.
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
// ❌ Don't: an action that calls another action
|
|
20
|
-
import { slackSendMessage } from "@keystrokehq/slack/actions";
|
|
21
|
-
export const notify = defineAction({ slug: "notify",
|
|
22
|
-
run: async (input) => slackSendMessage.run({ channel: input.channel, markdown_text: input.text }),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// ✅ Do: use the integration action directly as a workflow step
|
|
26
|
-
async run(input) {
|
|
27
|
-
return slackSendMessage.run({ channel: input.channel, markdown_text: buildText(input) });
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
This is enforced at runtime (calling an action inside an action throws) and by lint (`no-restricted-imports` blocks importing `@keystrokehq/*/actions` inside `src/actions/`). An action **may** call an agent — see below.
|
|
32
|
-
|
|
33
|
-
## Custom API actions (standalone credential)
|
|
34
|
-
|
|
35
|
-
The simplest custom integration: declare a `defineCredential` and consume it from a plain `defineAction`. No app needed.
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
import { defineAction } from "@keystrokehq/keystroke/action";
|
|
39
|
-
import { defineCredential } from "@keystrokehq/keystroke/credentials";
|
|
40
|
-
import { z } from "zod";
|
|
41
|
-
|
|
42
|
-
const acme = defineCredential({ key: "acme", fields: { apiKey: z.string() } });
|
|
43
|
-
|
|
44
|
-
export const createAcmeTicket = defineAction({
|
|
45
|
-
slug: "create-acme-ticket",
|
|
46
|
-
input: z.object({ title: z.string() }),
|
|
47
|
-
output: z.object({ id: z.string() }),
|
|
48
|
-
credentials: [acme] as const,
|
|
49
|
-
async run(input, credentials) {
|
|
50
|
-
return createTicket({ apiKey: credentials.acme.apiKey, title: input.title });
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Connect it with `keystroke credentials set acme --set apiKey=@env:ACME_API_KEY --scope org`.
|
|
56
|
-
|
|
57
|
-
## App-backed actions (shared connection / synced apps)
|
|
58
|
-
|
|
59
|
-
Reach for an app when several actions share one connection, or when you've synced a catalog/custom app:
|
|
60
|
-
|
|
61
|
-
1. **App first** — create in the dashboard or `keystroke app create`, then `keystroke app sync <slug>` → `src/apps/<name>/app.ts`
|
|
62
|
-
2. **Connect** — `keystroke connect <slug>` (see [apps skill](.agents/skills/keystroke-apps/SKILL.md))
|
|
63
|
-
3. **Action** — `app.action()` in `src/actions/`, reading `credentials[app.slug]` in `run`
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
import { z } from "zod";
|
|
67
|
-
import { kwatch } from "../apps/kwatch/app";
|
|
68
|
-
|
|
69
|
-
export const kwatchListAlerts = kwatch.action({
|
|
70
|
-
slug: "kwatch-list-alerts",
|
|
71
|
-
input: z.object({}),
|
|
72
|
-
output: z.object({ ok: z.boolean(), alertCount: z.number() }),
|
|
73
|
-
async run(_input, credentials) {
|
|
74
|
-
const { apiKey } = credentials["keystroke/kwatch"];
|
|
75
|
-
return { ok: true, alertCount: 0 };
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Use an app wrapper (over a standalone credential) when a family of actions should share one connection definition.
|
|
81
|
-
|
|
82
|
-
## Pure actions (no credential)
|
|
83
|
-
|
|
84
|
-
Use `defineAction` with no `credentials` when the step needs no connection at all (pure logic, local transforms):
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { defineAction } from "@keystrokehq/keystroke/action";
|
|
88
|
-
import { z } from "zod";
|
|
89
|
-
|
|
90
|
-
export const triage = defineAction({
|
|
91
|
-
slug: "triage",
|
|
92
|
-
name: "Triage",
|
|
93
|
-
description: "Classify an inbound message",
|
|
94
|
-
input: z.object({ message: z.string(), sender: z.string() }),
|
|
95
|
-
output: z.object({ priority: z.enum(["low", "normal", "high", "urgent"]) }),
|
|
96
|
-
run: async (input) => ({ priority: "normal" }),
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Catalog integration actions
|
|
101
|
-
|
|
102
|
-
Official integrations ship as npm packages with pre-built actions. Discover with `keystroke app search`, connect with `keystroke connect <slug>`, then import in workflows or agent tools — not in `src/actions/`:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
import { exaSearch } from "@keystrokehq/exa/actions";
|
|
106
|
-
import { slackSendMessage } from "@keystrokehq/slack/actions";
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Detail: [catalog-and-imports.md](references/catalog-and-imports.md).
|
|
110
|
-
|
|
111
|
-
## Call an agent from an action (allowed)
|
|
112
|
-
|
|
113
|
-
Agents — not other actions — are the one thing an action may invoke:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import signupResearcher from "../agents/signup-researcher";
|
|
117
|
-
|
|
118
|
-
export const researchSignup = defineAction({
|
|
119
|
-
slug: "research-signup",
|
|
120
|
-
run: async (input) => {
|
|
121
|
-
const result = await signupResearcher.prompt({ message: "…" });
|
|
122
|
-
return { brief: "…" };
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Credential scope & resolution
|
|
128
|
-
|
|
129
|
-
When a credential's scope isn't pinned, the runtime resolves it **project default first, then org default**. Pin a scope per-use with `.scope()` (the authoring-side counterpart to the CLI `--scope` flag) — it returns a fresh action that still binds like any other step/tool:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
await myAction.scope("user").run(input); // workflow step
|
|
133
|
-
tools: [myAction.scope("user")]; // agent tool
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
When several instances exist for one app + scope and none is the default, pin an exact instance to the consuming step/tool from the CLI: `keystroke credentials assignments assign --workflow <slug> --credential <app>/<slug> --consumer step:<slug>#0` (see [apps skill](.agents/skills/keystroke-apps/SKILL.md)).
|
|
137
|
-
|
|
138
|
-
## Long-running actions: honor the abort signal
|
|
139
|
-
|
|
140
|
-
`run` receives a third `ctx` argument with an `AbortSignal`. Thread it into fetches/SDK calls so the framework can cancel work cleanly:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
async run(input, credentials, ctx) {
|
|
144
|
-
const res = await fetch(url, { signal: ctx?.signal });
|
|
145
|
-
return res.json();
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Where actions run
|
|
150
|
-
|
|
151
|
-
| Consumer | Usage |
|
|
152
|
-
| -------- | ----------------------------------------------- |
|
|
153
|
-
| Workflow | `await myAction.run(input)` in `defineWorkflow` |
|
|
154
|
-
| Agent | `tools: [myAction, exaSearch]` on `defineAgent` |
|
|
155
|
-
|
|
156
|
-
## Next references
|
|
157
|
-
|
|
158
|
-
- [catalog-and-imports.md](references/catalog-and-imports.md) — catalog discovery, npm imports
|
|
159
|
-
|
|
160
|
-
Related: [apps](.agents/skills/keystroke-apps/SKILL.md), [workflows](.agents/skills/keystroke-workflows/SKILL.md), [agents](.agents/skills/keystroke-agents/SKILL.md).
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Integration actions
|
|
2
|
-
|
|
3
|
-
## Official catalog integrations
|
|
4
|
-
|
|
5
|
-
1. Discover: `keystroke app search <query>` → `app show <slug>` → `app actions <slug>`
|
|
6
|
-
2. Connect: `keystroke connect <slug>`
|
|
7
|
-
3. Add npm package (e.g. `@keystrokehq/exa`, `@keystrokehq/googlesuper`, `@keystrokehq/slack`)
|
|
8
|
-
4. Import in `src/workflows/` or on `defineAgent` `tools` — not in `src/actions/`:
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import { exaSearch, exaAnswer } from "@keystrokehq/exa/actions";
|
|
12
|
-
import { slackSendMessage } from "@keystrokehq/slack/actions";
|
|
13
|
-
import { googlesuperSendEmail } from "@keystrokehq/googlesuper/actions";
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Catalog packages ship a pre-built app + actions — you do not author `src/apps/` for these.
|
|
17
|
-
|
|
18
|
-
## Custom integrations
|
|
19
|
-
|
|
20
|
-
Two supported paths. Pick based on whether actions share a connection.
|
|
21
|
-
|
|
22
|
-
### Standalone credential (simplest)
|
|
23
|
-
|
|
24
|
-
Declare a `defineCredential` and consume it from a plain `defineAction`:
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { defineAction } from "@keystrokehq/keystroke/action";
|
|
28
|
-
import { defineCredential } from "@keystrokehq/keystroke/credentials";
|
|
29
|
-
import { z } from "zod";
|
|
30
|
-
|
|
31
|
-
const acme = defineCredential({ key: "acme", fields: { apiKey: z.string() } });
|
|
32
|
-
|
|
33
|
-
export const fetchStatus = defineAction({
|
|
34
|
-
slug: "fetch-status",
|
|
35
|
-
input: z.object({}),
|
|
36
|
-
output: z.object({ ok: z.boolean() }),
|
|
37
|
-
credentials: [acme] as const,
|
|
38
|
-
async run(_input, credentials) {
|
|
39
|
-
const { apiKey } = credentials.acme;
|
|
40
|
-
return { ok: true };
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Connect it with `keystroke credentials set acme --set apiKey=@env:ACME_API_KEY --scope org`.
|
|
46
|
-
|
|
47
|
-
### App wrapper (shared connection / synced apps)
|
|
48
|
-
|
|
49
|
-
Use an app when several actions share one connection, or when you've synced a custom/catalog app:
|
|
50
|
-
|
|
51
|
-
1. Create the app (dashboard or `keystroke app create --name ... --field ...`)
|
|
52
|
-
2. Sync: `keystroke app sync <slug>` → `src/apps/<name>/app.ts`
|
|
53
|
-
3. Connect: `keystroke connect <slug>`
|
|
54
|
-
4. Author actions in `src/actions/` with `app.action()`:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import { z } from "zod";
|
|
58
|
-
import { internalApi } from "../apps/internal-api/app";
|
|
59
|
-
|
|
60
|
-
export const fetchStatus = internalApi.action({
|
|
61
|
-
slug: "fetch-status",
|
|
62
|
-
input: z.object({}),
|
|
63
|
-
output: z.object({ ok: z.boolean() }),
|
|
64
|
-
async run(_input, credentials) {
|
|
65
|
-
const { apiKey } = credentials["acme/internal-api"];
|
|
66
|
-
return { ok: true };
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Re-run `keystroke app sync <slug>` after template changes on the platform. Full app lifecycle: [apps skill](../../keystroke-apps/SKILL.md).
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: keystroke-agents
|
|
3
|
-
description: Build keystroke agents with defineAgent — models, integration tools (Exa, Google, Slack), skills, files, and sandbox. Use when authoring src/agents/ or debugging agent sessions.
|
|
4
|
-
metadata:
|
|
5
|
-
keystroke-domain: agents
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Agents
|
|
9
|
-
|
|
10
|
-
Agents are **autonomous LLM runs** with a sandbox, optional skills/files, and **tools** (your actions + integration actions).
|
|
11
|
-
|
|
12
|
-
## Typical patterns
|
|
13
|
-
|
|
14
|
-
**Integration tools** — import actions from an integration package:
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import { defineAgent } from "@keystrokehq/keystroke/agent";
|
|
18
|
-
import { exaSearch, exaAnswer } from "@keystrokehq/exa/actions";
|
|
19
|
-
|
|
20
|
-
export default defineAgent({
|
|
21
|
-
slug: "signup-researcher",
|
|
22
|
-
systemPrompt: "Research signups with Exa. Stay concise; cite sources.",
|
|
23
|
-
model: "anthropic/claude-sonnet-4.6",
|
|
24
|
-
tools: [exaSearch, exaAnswer],
|
|
25
|
-
});
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Skills + files** — static playbooks and docs in the workspace:
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
import { defineAgent } from "@keystrokehq/keystroke/agent";
|
|
32
|
-
import { defineSandbox } from "@keystrokehq/keystroke/sandbox";
|
|
33
|
-
|
|
34
|
-
defineAgent({
|
|
35
|
-
slug: "support",
|
|
36
|
-
systemPrompt: "Read /workspace/agent/product-guide.md before answering.",
|
|
37
|
-
skills: ["support"],
|
|
38
|
-
sandbox: defineSandbox({ files: true }),
|
|
39
|
-
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Import actions from `@keystrokehq/<integration>/actions` (e.g. `@keystrokehq/exa/actions`, `@keystrokehq/googlesuper/actions`).
|
|
43
|
-
|
|
44
|
-
## Built-in capabilities (on by default)
|
|
45
|
-
|
|
46
|
-
Two capabilities are enabled automatically — you don't add them to `tools`:
|
|
47
|
-
|
|
48
|
-
- **Memory** — agents persist memory across sessions by default. Disable with `memory: false`.
|
|
49
|
-
- **Web** — built-in `web_search` / `web_fetch` host tools are available by default. Disable with `web: false`.
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
defineAgent({
|
|
53
|
-
slug: "researcher",
|
|
54
|
-
systemPrompt: "…",
|
|
55
|
-
model: "anthropic/claude-sonnet-4.6",
|
|
56
|
-
memory: false, // opt out of persistent memory
|
|
57
|
-
web: false, // opt out of built-in web tools
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Delegate to other agents with subagents (`defineSubagentTool`) — see [tools-mcp-codemode.md](references/tools-mcp-codemode.md).
|
|
62
|
-
|
|
63
|
-
## Structured output (`outputSchema`)
|
|
64
|
-
|
|
65
|
-
`outputSchema` is a **per-prompt** option, not a `defineAgent` field — the same agent can return free text on one call and a schema-validated object on the next. Pass a Zod schema to `prompt(...)` and read the parsed result from `result.output` (typed from the schema):
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import { z } from "zod";
|
|
69
|
-
import researcher from "../agents/signup-researcher";
|
|
70
|
-
|
|
71
|
-
const Summary = z.object({ company: z.string(), summary: z.string() });
|
|
72
|
-
|
|
73
|
-
const result = await researcher.prompt({
|
|
74
|
-
message: "Research Acme Corp",
|
|
75
|
-
outputSchema: Summary,
|
|
76
|
-
});
|
|
77
|
-
if (result.error) throw new Error(result.error);
|
|
78
|
-
|
|
79
|
-
const { company, summary } = result.output!; // typed { company: string; summary: string }
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Without `outputSchema`, `result.output` is `undefined` and you read the reply from `result.messages`. This is an in-process TypeScript API (workflows, actions, scripts) — structured output is **not** exposed over the HTTP route or `keystroke agent prompt`.
|
|
83
|
-
|
|
84
|
-
## Run & audit
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
keystroke agent prompt signup-researcher --message "Research Acme Corp"
|
|
88
|
-
keystroke agent sessions list signup-researcher
|
|
89
|
-
keystroke agent sessions get signup-researcher <session-id> --include messages,trace
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Follow up in the same session: `--session-id <id>`.
|
|
93
|
-
|
|
94
|
-
## How agents get invoked
|
|
95
|
-
|
|
96
|
-
| From | How |
|
|
97
|
-
| --------------- | --------------------------------------------------------- |
|
|
98
|
-
| CLI | `keystroke agent prompt {slug} --message "…"` |
|
|
99
|
-
| Workflow action | `await myAgent.prompt({ message })` inside `defineAction` |
|
|
100
|
-
| Channel | Slack message to a bound agent (see channels skill) |
|
|
101
|
-
|
|
102
|
-
The agent's identity field is `slug` — it's also the route id (`POST /agents/{slug}`) and the CLI `<agentId>` argument.
|
|
103
|
-
|
|
104
|
-
## Workspace
|
|
105
|
-
|
|
106
|
-
- Skills → `/workspace/agent/skills/{skill-name}/` (the skill's folder name)
|
|
107
|
-
- Files from `src/files/{slug}/` → `/workspace/agent/` (`{slug}` is the agent slug when `files: true`)
|
|
108
|
-
|
|
109
|
-
## Next references
|
|
110
|
-
|
|
111
|
-
- [models.md](references/models.md) — model ids, platform LLM proxy
|
|
112
|
-
- [tools-mcp-codemode.md](references/tools-mcp-codemode.md) — actions as tools, MCP, codemode
|
|
113
|
-
- [workflows-and-testing.md](references/workflows-and-testing.md) — sessions, workflow handoff
|
|
114
|
-
|
|
115
|
-
Related: [actions](.agents/skills/keystroke-actions/SKILL.md), [workflows](.agents/skills/keystroke-workflows/SKILL.md), [files](.agents/skills/keystroke-files/SKILL.md), [apps](.agents/skills/keystroke-apps/SKILL.md).
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Models
|
|
2
|
-
|
|
3
|
-
```ts
|
|
4
|
-
model: "google/gemini-2.5-flash"; // vendor/model-id — any pi-ai or gateway catalog id
|
|
5
|
-
thinkingLevel: "high"; // optional — defaults to "medium"; use "none" to disable reasoning
|
|
6
|
-
```
|
|
7
|
-
|
|
8
|
-
`thinkingLevel` accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh` (`off` disables reasoning).
|
|
9
|
-
|
|
10
|
-
Per-prompt override via API: `{ "message": "...", "thinkingLevel": "low" }`.
|
|
11
|
-
|
|
12
|
-
Set `model` to any `vendor/model-id` and deploy — the platform routes agents through its managed LLM proxy automatically, so there are **no provider keys to configure**.
|
|
13
|
-
|
|
14
|
-
| Vendor | Example id |
|
|
15
|
-
| --------- | ----------------------------- |
|
|
16
|
-
| Anthropic | `anthropic/claude-sonnet-4.5` |
|
|
17
|
-
| OpenAI | `openai/gpt-5.5` |
|
|
18
|
-
| Google | `google/gemini-3.5-flash` |
|
|
19
|
-
| DeepSeek | `deepseek/deepseek-v3` |
|
|
20
|
-
| Moonshot | `moonshotai/kimi-k2.5` |
|
|
21
|
-
| Zhipu GLM | `zai/glm-4.5` |
|
|
22
|
-
|
|
23
|
-
After changing model, smoke-test: `keystroke agent prompt <key> --message "Hi"`.
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Tools, MCP, and codemode
|
|
2
|
-
|
|
3
|
-
## Integration actions as tools
|
|
4
|
-
|
|
5
|
-
Most agents use imported integration actions:
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
import { exaSearch, exaAnswer } from "@keystrokehq/exa/actions";
|
|
9
|
-
import { googlesuperFetchEmails, googlesuperSendEmail } from "@keystrokehq/googlesuper/actions";
|
|
10
|
-
|
|
11
|
-
tools: [exaSearch, exaAnswer, googlesuperFetchEmails, googlesuperSendEmail],
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
See each integration package's `actions` export for available tools.
|
|
15
|
-
|
|
16
|
-
## Your own actions as tools
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { triage } from "../actions/triage";
|
|
20
|
-
tools: [triage],
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Same action works in workflows and on agents.
|
|
24
|
-
|
|
25
|
-
## Subagents (delegate to another agent)
|
|
26
|
-
|
|
27
|
-
Wrap any agent as a tool with `defineSubagentTool` (from `@keystrokehq/keystroke/agent`) and add it to `tools`. The parent delegates a scoped task to the child agent:
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { defineAgent, defineSubagentTool } from "@keystrokehq/keystroke/agent";
|
|
31
|
-
import { z } from "zod";
|
|
32
|
-
import researcher from "./researcher";
|
|
33
|
-
|
|
34
|
-
export default defineAgent({
|
|
35
|
-
slug: "planner",
|
|
36
|
-
systemPrompt: "Plan work. Delegate research to the researcher subagent.",
|
|
37
|
-
model: "anthropic/claude-sonnet-4.6",
|
|
38
|
-
tools: [
|
|
39
|
-
defineSubagentTool({
|
|
40
|
-
agent: researcher,
|
|
41
|
-
name: "research",
|
|
42
|
-
label: "Research a topic",
|
|
43
|
-
parameters: z.object({ message: z.string() }), // default toMessage reads params.message
|
|
44
|
-
}),
|
|
45
|
-
],
|
|
46
|
-
});
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## MCP
|
|
50
|
-
|
|
51
|
-
Point an agent at an MCP server with `defineMcp` (from `@keystrokehq/keystroke/agent`) and add the definition to `tools` alongside actions. Each tool the server lists becomes available, prefixed with the server `key` (e.g. `mcp__deepwiki__ask_question`).
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import { defineAgent, defineMcp } from "@keystrokehq/keystroke/agent";
|
|
55
|
-
|
|
56
|
-
const deepwiki = defineMcp({
|
|
57
|
-
key: "deepwiki",
|
|
58
|
-
transport: { type: "http", url: "https://mcp.deepwiki.com/mcp" },
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
export default defineAgent({
|
|
62
|
-
slug: "researcher",
|
|
63
|
-
systemPrompt: "Use the DeepWiki tools to answer questions about repos.",
|
|
64
|
-
model: "anthropic/claude-sonnet-4.6",
|
|
65
|
-
tools: [deepwiki],
|
|
66
|
-
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
For servers that require auth, declare credentials on the definition.
|
|
70
|
-
|
|
71
|
-
## Codemode (advanced)
|
|
72
|
-
|
|
73
|
-
Default sandbox has bash. For batch tool calls, agents can run js-exec scripts that call `await tools['action-slug'](args)` (the tool name is the action's slug; MCP tools keep their `mcp__<key>__<tool>` prefix). Optional VM runtime via `sandbox: defineSandbox({ mode: "vm" })` (from `@keystrokehq/keystroke/sandbox`).
|