context101-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/context101.js +11 -0
- package/package.json +26 -0
- package/src/amplify-repo.js +25 -0
- package/src/aws-profiles.js +128 -0
- package/src/bedrock-access.js +129 -0
- package/src/cdk-invoke.js +231 -0
- package/src/checks.js +165 -0
- package/src/clone.js +63 -0
- package/src/config.js +102 -0
- package/src/defaults.js +28 -0
- package/src/deploy-env-load.js +74 -0
- package/src/deploy.js +138 -0
- package/src/docker.js +120 -0
- package/src/embedding-models.js +184 -0
- package/src/env-file.js +140 -0
- package/src/exec.js +34 -0
- package/src/hosted-url.js +35 -0
- package/src/init.js +435 -0
- package/src/main.js +41 -0
- package/src/parse-args.js +320 -0
- package/src/plan.js +111 -0
- package/src/prompt.js +104 -0
- package/src/redact.js +28 -0
- package/src/repo.js +79 -0
- package/src/secrets.js +9 -0
- package/src/stacks.js +187 -0
- package/src/style.js +54 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Keep in sync with web/lib/embedding-models.ts — ListFoundationModels
|
|
2
|
+
// does not return vector dimensions, and SKU variants (…:0:512) are not
|
|
3
|
+
// valid Knowledge Base embeddingModelArn values.
|
|
4
|
+
export const KNOWN_EMBEDDING_DIMENSIONS = {
|
|
5
|
+
"amazon.titan-embed-text-v2:0": {
|
|
6
|
+
supportedDimensions: [256, 512, 1024],
|
|
7
|
+
defaultDimension: 1024,
|
|
8
|
+
label: "Titan Text Embeddings V2",
|
|
9
|
+
provider: "aws",
|
|
10
|
+
},
|
|
11
|
+
"amazon.titan-embed-text-v1": {
|
|
12
|
+
supportedDimensions: [1536],
|
|
13
|
+
defaultDimension: 1536,
|
|
14
|
+
label: "Titan Embeddings G1 - Text",
|
|
15
|
+
provider: "aws",
|
|
16
|
+
},
|
|
17
|
+
"amazon.titan-embed-image-v1": {
|
|
18
|
+
supportedDimensions: [1024],
|
|
19
|
+
defaultDimension: 1024,
|
|
20
|
+
label: "Titan Multimodal Embeddings G1",
|
|
21
|
+
provider: "aws",
|
|
22
|
+
},
|
|
23
|
+
"cohere.embed-english-v3": {
|
|
24
|
+
supportedDimensions: [1024],
|
|
25
|
+
defaultDimension: 1024,
|
|
26
|
+
label: "Embed English v3",
|
|
27
|
+
provider: "cohere",
|
|
28
|
+
},
|
|
29
|
+
"cohere.embed-multilingual-v3": {
|
|
30
|
+
supportedDimensions: [1024],
|
|
31
|
+
defaultDimension: 1024,
|
|
32
|
+
label: "Embed Multilingual v3",
|
|
33
|
+
provider: "cohere",
|
|
34
|
+
},
|
|
35
|
+
"cohere.embed-english-light-v3": {
|
|
36
|
+
supportedDimensions: [384],
|
|
37
|
+
defaultDimension: 384,
|
|
38
|
+
label: "Embed English Light v3",
|
|
39
|
+
provider: "cohere",
|
|
40
|
+
},
|
|
41
|
+
"cohere.embed-multilingual-light-v3": {
|
|
42
|
+
supportedDimensions: [384],
|
|
43
|
+
defaultDimension: 384,
|
|
44
|
+
label: "Embed Multilingual Light v3",
|
|
45
|
+
provider: "cohere",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function isKnownEmbeddingModel(modelId) {
|
|
50
|
+
return Boolean(modelId && KNOWN_EMBEDDING_DIMENSIONS[modelId]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function embeddingModelMeta(modelId) {
|
|
54
|
+
return KNOWN_EMBEDDING_DIMENSIONS[modelId] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fallbackEmbeddingModels() {
|
|
58
|
+
return Object.keys(KNOWN_EMBEDDING_DIMENSIONS).map((id) => catalogEntry(id));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Live Bedrock list plus every curated embedding id brains can pick later. */
|
|
62
|
+
export function allEmbeddingModels(listed = []) {
|
|
63
|
+
const byId = new Map();
|
|
64
|
+
for (const id of Object.keys(KNOWN_EMBEDDING_DIMENSIONS)) {
|
|
65
|
+
byId.set(id, catalogEntry(id));
|
|
66
|
+
}
|
|
67
|
+
for (const model of listed) {
|
|
68
|
+
if (model?.id) byId.set(model.id, model);
|
|
69
|
+
}
|
|
70
|
+
return [...byId.values()].sort((a, b) =>
|
|
71
|
+
a.provider === b.provider
|
|
72
|
+
? a.label.localeCompare(b.label)
|
|
73
|
+
: a.provider.localeCompare(b.provider)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function catalogEntry(modelId, extras = {}) {
|
|
78
|
+
const meta = KNOWN_EMBEDDING_DIMENSIONS[modelId];
|
|
79
|
+
if (meta) {
|
|
80
|
+
return {
|
|
81
|
+
id: modelId,
|
|
82
|
+
provider: meta.provider,
|
|
83
|
+
label: meta.label,
|
|
84
|
+
dimensions: meta.defaultDimension,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
id: modelId,
|
|
89
|
+
provider: extras.provider || providerFromId(modelId),
|
|
90
|
+
label: extras.label || modelId,
|
|
91
|
+
dimensions: extras.dimensions ?? null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatEmbeddingChoice(model) {
|
|
96
|
+
return `${model.label} (${model.id}, ${model.dimensions})`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function providerFromName(name) {
|
|
100
|
+
if (name === "Amazon") return "aws";
|
|
101
|
+
if (name === "Cohere") return "cohere";
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function providerFromId(id) {
|
|
106
|
+
if (String(id).startsWith("amazon.")) return "aws";
|
|
107
|
+
if (String(id).startsWith("cohere.")) return "cohere";
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** SKU variants (…:0:512) are not valid Knowledge Base embeddingModelArn values. */
|
|
112
|
+
export function isEmbeddingSkuVariant(modelId) {
|
|
113
|
+
return (String(modelId).match(/:/g) || []).length >= 2;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseEmbeddingCatalog(payload) {
|
|
117
|
+
const summaries = payload?.modelSummaries;
|
|
118
|
+
if (!Array.isArray(summaries)) return [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const models = [];
|
|
121
|
+
for (const row of summaries) {
|
|
122
|
+
const id = row?.modelId;
|
|
123
|
+
if (!id || seen.has(id)) continue;
|
|
124
|
+
if (row.modelLifecycle?.status && row.modelLifecycle.status !== "ACTIVE") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (isEmbeddingSkuVariant(id)) continue;
|
|
128
|
+
const provider = providerFromName(row.providerName) || providerFromId(id);
|
|
129
|
+
if (provider !== "aws" && provider !== "cohere") continue;
|
|
130
|
+
seen.add(id);
|
|
131
|
+
models.push(catalogEntry(id, { provider, label: row.modelName }));
|
|
132
|
+
}
|
|
133
|
+
models.sort((a, b) =>
|
|
134
|
+
a.provider === b.provider
|
|
135
|
+
? a.label.localeCompare(b.label)
|
|
136
|
+
: a.provider.localeCompare(b.provider)
|
|
137
|
+
);
|
|
138
|
+
return models;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function listEmbeddingModels({ exec, env, region } = {}) {
|
|
142
|
+
if (!exec || !region) {
|
|
143
|
+
return { models: fallbackEmbeddingModels(), source: "fallback", warning: null };
|
|
144
|
+
}
|
|
145
|
+
const result = exec({
|
|
146
|
+
command: "aws",
|
|
147
|
+
args: [
|
|
148
|
+
"bedrock",
|
|
149
|
+
"list-foundation-models",
|
|
150
|
+
"--by-output-modality",
|
|
151
|
+
"EMBEDDING",
|
|
152
|
+
"--region",
|
|
153
|
+
region,
|
|
154
|
+
"--output",
|
|
155
|
+
"json",
|
|
156
|
+
],
|
|
157
|
+
env,
|
|
158
|
+
});
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
return {
|
|
161
|
+
models: fallbackEmbeddingModels(),
|
|
162
|
+
source: "fallback",
|
|
163
|
+
warning: "Could not list Bedrock embedding models — showing defaults.",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const listed = parseEmbeddingCatalog(JSON.parse(result.stdout || "{}"));
|
|
168
|
+
const models = allEmbeddingModels(listed);
|
|
169
|
+
if (listed.length === 0) {
|
|
170
|
+
return {
|
|
171
|
+
models,
|
|
172
|
+
source: "fallback",
|
|
173
|
+
warning: "No embedding models returned by Bedrock — showing defaults.",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { models, source: "live", warning: null };
|
|
177
|
+
} catch {
|
|
178
|
+
return {
|
|
179
|
+
models: fallbackEmbeddingModels(),
|
|
180
|
+
source: "fallback",
|
|
181
|
+
warning: "Could not list Bedrock embedding models — showing defaults.",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/env-file.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ALLOW_PUBLIC_SIGNUP,
|
|
5
|
+
APP_MODE,
|
|
6
|
+
BILLING_ENABLED,
|
|
7
|
+
DRIVER_NEON,
|
|
8
|
+
DRIVER_POSTGRES,
|
|
9
|
+
} from "./defaults.js";
|
|
10
|
+
import { ownPublicUrl } from "./hosted-url.js";
|
|
11
|
+
|
|
12
|
+
export function inferDriver(url) {
|
|
13
|
+
if (!url) return DRIVER_POSTGRES;
|
|
14
|
+
return url.includes("neon.tech") ? DRIVER_NEON : DRIVER_POSTGRES;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function inferPrepare(url) {
|
|
18
|
+
if (!url) return true;
|
|
19
|
+
return !/pooler\.supabase\.com/i.test(url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function quoteShell(value) {
|
|
23
|
+
return `"${String(value).replace(/["\\$`]/g, "\\$&")}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderDeployEnv(values) {
|
|
27
|
+
const lines = [
|
|
28
|
+
"# Written by `context101 init`. Gitignored. chmod 600.",
|
|
29
|
+
"# Deploy with `context101 deploy` — never raw `cdk deploy`.",
|
|
30
|
+
"",
|
|
31
|
+
`CTX_TOKEN=${quoteShell(values.CTX_TOKEN)}`,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
if (values.CTX_GH_TOKEN) {
|
|
35
|
+
lines.push(`CTX_GH_TOKEN=${quoteShell(values.CTX_GH_TOKEN)}`);
|
|
36
|
+
} else {
|
|
37
|
+
lines.push(
|
|
38
|
+
"# CTX_GH_TOKEN omitted — only needed if REPOSITORY is set (Amplify watches a repo)."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.push("");
|
|
43
|
+
if (values.AWS_PROFILE) {
|
|
44
|
+
lines.push(`AWS_PROFILE=${quoteShell(values.AWS_PROFILE)}`);
|
|
45
|
+
}
|
|
46
|
+
if (values.AWS_ACCESS_KEY_ID) {
|
|
47
|
+
lines.push(`AWS_ACCESS_KEY_ID=${quoteShell(values.AWS_ACCESS_KEY_ID)}`);
|
|
48
|
+
}
|
|
49
|
+
if (values.AWS_SECRET_ACCESS_KEY) {
|
|
50
|
+
lines.push(`AWS_SECRET_ACCESS_KEY=${quoteShell(values.AWS_SECRET_ACCESS_KEY)}`);
|
|
51
|
+
}
|
|
52
|
+
if (values.AWS_REGION) {
|
|
53
|
+
lines.push(`AWS_REGION=${quoteShell(values.AWS_REGION)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push("");
|
|
57
|
+
if (values.CREATE_RDS) {
|
|
58
|
+
lines.push("# DATABASE_URL omitted — CDK creates RDS Postgres.");
|
|
59
|
+
lines.push(`CREATE_RDS=${quoteShell("true")}`);
|
|
60
|
+
lines.push(
|
|
61
|
+
`DATABASE_DRIVER=${quoteShell(values.DATABASE_DRIVER ?? DRIVER_POSTGRES)}`
|
|
62
|
+
);
|
|
63
|
+
lines.push(`DATABASE_PREPARE=${quoteShell("true")}`);
|
|
64
|
+
} else {
|
|
65
|
+
lines.push(`DATABASE_URL=${quoteShell(values.DATABASE_URL)}`);
|
|
66
|
+
lines.push(`DATABASE_DRIVER=${quoteShell(values.DATABASE_DRIVER)}`);
|
|
67
|
+
lines.push(
|
|
68
|
+
`DATABASE_PREPARE=${quoteShell(values.DATABASE_PREPARE ? "true" : "false")}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push(`BETTER_AUTH_SECRET=${quoteShell(values.BETTER_AUTH_SECRET)}`);
|
|
74
|
+
lines.push(
|
|
75
|
+
"# BETTER_AUTH_URL / APP_URL: omit to use Amplify's default domain"
|
|
76
|
+
);
|
|
77
|
+
lines.push(
|
|
78
|
+
"# (https://main.<app-id>.amplifyapp.com), or set a domain you own."
|
|
79
|
+
);
|
|
80
|
+
lines.push("# Never the hosted Context101 product.");
|
|
81
|
+
const betterAuthUrl = ownPublicUrl(values.BETTER_AUTH_URL);
|
|
82
|
+
const appUrl = ownPublicUrl(values.APP_URL);
|
|
83
|
+
if (betterAuthUrl) {
|
|
84
|
+
lines.push(`BETTER_AUTH_URL=${quoteShell(betterAuthUrl)}`);
|
|
85
|
+
}
|
|
86
|
+
if (appUrl) {
|
|
87
|
+
lines.push(`APP_URL=${quoteShell(appUrl)}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push(`MCP_TOKEN_PEPPER=${quoteShell(values.MCP_TOKEN_PEPPER)}`);
|
|
90
|
+
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push(`APP_MODE=${quoteShell(values.APP_MODE ?? APP_MODE)}`);
|
|
93
|
+
lines.push(
|
|
94
|
+
`ALLOW_PUBLIC_SIGNUP=${quoteShell(values.ALLOW_PUBLIC_SIGNUP ?? ALLOW_PUBLIC_SIGNUP)}`
|
|
95
|
+
);
|
|
96
|
+
lines.push(
|
|
97
|
+
`BILLING_ENABLED=${quoteShell(values.BILLING_ENABLED ?? BILLING_ENABLED)}`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (values.REPOSITORY) {
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(`REPOSITORY=${quoteShell(values.REPOSITORY)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (values.EMBED_MODEL_ID) {
|
|
106
|
+
lines.push(`EMBED_MODEL_ID=${quoteShell(values.EMBED_MODEL_ID)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push("");
|
|
110
|
+
return `${lines.join("\n")}\n`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function writeDeployEnv(filePath, values) {
|
|
114
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
115
|
+
await writeFile(filePath, renderDeployEnv(values), { encoding: "utf8", mode: 0o600 });
|
|
116
|
+
await chmod(filePath, 0o600);
|
|
117
|
+
return filePath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function readExampleToken(examplePath) {
|
|
121
|
+
try {
|
|
122
|
+
const text = await readFile(examplePath, "utf8");
|
|
123
|
+
const match = text.match(/^CTX_TOKEN=(.*)$/m);
|
|
124
|
+
if (!match) return null;
|
|
125
|
+
return unquote(match[1]);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unquote(raw) {
|
|
132
|
+
const trimmed = raw.trim();
|
|
133
|
+
if (
|
|
134
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
135
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
136
|
+
) {
|
|
137
|
+
return trimmed.slice(1, -1);
|
|
138
|
+
}
|
|
139
|
+
return trimmed;
|
|
140
|
+
}
|
package/src/exec.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { runCdk } from "./cdk-invoke.js";
|
|
3
|
+
|
|
4
|
+
export function createExec(baseEnv = process.env) {
|
|
5
|
+
return function exec({ command, args = [], env, cwd, timeout = 15_000 }) {
|
|
6
|
+
const result = spawnSync(command, args, {
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
timeout,
|
|
9
|
+
env: { ...baseEnv, ...env },
|
|
10
|
+
cwd,
|
|
11
|
+
});
|
|
12
|
+
const stdout = result.stdout ?? "";
|
|
13
|
+
const stderr = result.stderr ?? "";
|
|
14
|
+
const code = result.status ?? (result.error ? 1 : 0);
|
|
15
|
+
return {
|
|
16
|
+
ok: code === 0,
|
|
17
|
+
code,
|
|
18
|
+
stdout: stdout.trim(),
|
|
19
|
+
stderr: stderr.trim(),
|
|
20
|
+
error: result.error ?? null,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function commandExists(exec, name) {
|
|
26
|
+
const result = exec({ command: "sh", args: ["-c", `command -v ${name}`] });
|
|
27
|
+
return result.ok && Boolean(result.stdout);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function runDeployWrapper(opts) {
|
|
31
|
+
return runCdk(opts);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { spawn };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const HOSTED_ZONE = "context101.dev";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The hosted product lives on this zone. Self-host must use the operator's
|
|
5
|
+
* domain or Amplify's default `main.<app-id>.amplifyapp.com`.
|
|
6
|
+
*/
|
|
7
|
+
export function isHostedContext101Url(raw) {
|
|
8
|
+
if (raw == null) return false;
|
|
9
|
+
const value = String(raw).trim();
|
|
10
|
+
if (!value) return false;
|
|
11
|
+
const host = hostnameOf(value);
|
|
12
|
+
return host === HOSTED_ZONE || host.endsWith(`.${HOSTED_ZONE}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ownPublicUrl(raw) {
|
|
16
|
+
if (raw == null) return undefined;
|
|
17
|
+
const value = String(raw).trim();
|
|
18
|
+
if (!value || isHostedContext101Url(value)) return undefined;
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hostnameOf(value) {
|
|
23
|
+
try {
|
|
24
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
|
|
25
|
+
? value
|
|
26
|
+
: `https://${value}`;
|
|
27
|
+
return new URL(withScheme).hostname.toLowerCase();
|
|
28
|
+
} catch {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
|
|
31
|
+
.split("/")[0]
|
|
32
|
+
.split(":")[0]
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
}
|