@terminus-ai/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +1055 -0
- package/bin/agent-discovery.mjs +71 -0
- package/bin/agent-icon.mjs +77 -0
- package/bin/agent-models.mjs +77 -0
- package/bin/agent-type.mjs +51 -0
- package/bin/agentdev.mjs +657 -0
- package/bin/app-route-script.mjs +59 -0
- package/bin/app-runtime-contract.mjs +2 -0
- package/bin/appdev-remote.mjs +346 -0
- package/bin/appdev.mjs +4446 -0
- package/bin/apps.mjs +5512 -0
- package/bin/capability-calls.mjs +437 -0
- package/bin/capsule-data.mjs +260 -0
- package/bin/client.mjs +189 -0
- package/bin/commands.mjs +1194 -0
- package/bin/dev-capsules.mjs +1599 -0
- package/bin/dev-contract.mjs +262 -0
- package/bin/dev-data.mjs +287 -0
- package/bin/dev-members.mjs +18 -0
- package/bin/dev-net.mjs +316 -0
- package/bin/dev-notification-popup.mjs +628 -0
- package/bin/dev-ports.mjs +567 -0
- package/bin/dev-server-binding.mjs +35 -0
- package/bin/dev-server-ops.mjs +1086 -0
- package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
- package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
- package/bin/dev-ui/OFL.txt +92 -0
- package/bin/dev-ui/agent-robot.webp +0 -0
- package/bin/dev-ui/app.js +5217 -0
- package/bin/dev-ui/highlight.js +195 -0
- package/bin/dev-ui/index.html +34 -0
- package/bin/dev-ui/style.css +3640 -0
- package/bin/devlint.mjs +112 -0
- package/bin/devserver.mjs +2127 -0
- package/bin/devtriggers.mjs +367 -0
- package/bin/endpoints.mjs +156 -0
- package/bin/errors.mjs +61 -0
- package/bin/files.mjs +169 -0
- package/bin/horizontal-capabilities/v1/contract.json +280 -0
- package/bin/http.mjs +500 -0
- package/bin/lint-manifests/justbash-commands.json +88 -0
- package/bin/lint-manifests/python-stdlib.json +295 -0
- package/bin/login-page.mjs +488 -0
- package/bin/schedules.mjs +664 -0
- package/bin/server-sandbox.mjs +204 -0
- package/bin/servicedev.mjs +425 -0
- package/bin/sync.mjs +357 -0
- package/bin/terminus.js +3666 -0
- package/bin/toolchain.mjs +125 -0
- package/bin/vendor/app-runtime-v1/app-host.json +124 -0
- package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
- package/bin/vendor/app-runtime-v1/doors.json +2867 -0
- package/bin/vendor/appd/node-harness.mjs +209 -0
- package/bin/vendor/appd/python-harness.py +12 -0
- package/bin/vendor/appd/server-protocol.json +84 -0
- package/bin/vendor/where.mjs +541 -0
- package/bin/versioning.mjs +72 -0
- package/bin/write-rules.mjs +398 -0
- package/package.json +41 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `discovery` section of an agent's terminus.json: whether, beyond what
|
|
3
|
+
* its `tools` name, the agent may go looking in the Store during a
|
|
4
|
+
* conversation.
|
|
5
|
+
*
|
|
6
|
+
* "discovery": {
|
|
7
|
+
* "skills": "store" | "pinned",
|
|
8
|
+
* "services": "store" | "pinned",
|
|
9
|
+
* "agents": "store" | "pinned"
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* "store" — it may search the Store and ask the person before using
|
|
13
|
+
* something it was not given.
|
|
14
|
+
* "pinned" — only what `tools` names, which it uses without asking.
|
|
15
|
+
*
|
|
16
|
+
* An absent kind means the platform's behaviour today, which is what the
|
|
17
|
+
* defaults below describe: pinning any skill already lets an agent search the
|
|
18
|
+
* marketplace and load any open skill; services are an exact allowlist; a
|
|
19
|
+
* published agent cannot call other agents at all (only Norbert can).
|
|
20
|
+
*
|
|
21
|
+
* The section is authored intent the platform does not read yet, so like
|
|
22
|
+
* `models` it is validated here and dropped from the wire manifest.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { CliError } from "./client.mjs";
|
|
26
|
+
|
|
27
|
+
export const DISCOVERY_KINDS = ["skills", "services", "agents"];
|
|
28
|
+
export const DISCOVERY_MODES = ["store", "pinned"];
|
|
29
|
+
export const DISCOVERY_DEFAULTS = { skills: "store", services: "pinned", agents: "pinned" };
|
|
30
|
+
|
|
31
|
+
export function readDiscovery(manifest = {}) {
|
|
32
|
+
const section = manifest.discovery && typeof manifest.discovery === "object" && !Array.isArray(manifest.discovery)
|
|
33
|
+
? manifest.discovery
|
|
34
|
+
: {};
|
|
35
|
+
return Object.fromEntries(DISCOVERY_KINDS.map((kind) => [
|
|
36
|
+
kind,
|
|
37
|
+
DISCOVERY_MODES.includes(section[kind]) ? section[kind] : DISCOVERY_DEFAULTS[kind],
|
|
38
|
+
]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateDiscovery(manifest = {}) {
|
|
42
|
+
if (!Object.hasOwn(manifest, "discovery")) return;
|
|
43
|
+
const section = manifest.discovery;
|
|
44
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
45
|
+
throw new CliError("terminus.json discovery must be an object of skills|services|agents modes");
|
|
46
|
+
}
|
|
47
|
+
for (const [kind, mode] of Object.entries(section)) {
|
|
48
|
+
if (!DISCOVERY_KINDS.includes(kind)) {
|
|
49
|
+
throw new CliError(`terminus.json discovery has no '${kind}' (use ${DISCOVERY_KINDS.join(", ")})`);
|
|
50
|
+
}
|
|
51
|
+
if (!DISCOVERY_MODES.includes(mode)) {
|
|
52
|
+
throw new CliError(`terminus.json discovery.${kind} is "store" or "pinned"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Set one kind's mode, writing only what differs from the default. */
|
|
58
|
+
export function writeDiscovery(manifest, kind, mode) {
|
|
59
|
+
if (!DISCOVERY_KINDS.includes(kind)) throw new CliError(`discovery has no '${kind}'`);
|
|
60
|
+
if (!DISCOVERY_MODES.includes(mode)) throw new CliError(`discovery.${kind} is "store" or "pinned"`);
|
|
61
|
+
const current = readDiscovery(manifest);
|
|
62
|
+
current[kind] = mode;
|
|
63
|
+
const section = Object.fromEntries(
|
|
64
|
+
DISCOVERY_KINDS
|
|
65
|
+
.filter((entry) => current[entry] !== DISCOVERY_DEFAULTS[entry])
|
|
66
|
+
.map((entry) => [entry, current[entry]]),
|
|
67
|
+
);
|
|
68
|
+
if (Object.keys(section).length) manifest.discovery = section;
|
|
69
|
+
else delete manifest.discovery;
|
|
70
|
+
return manifest;
|
|
71
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An agent's mark.
|
|
3
|
+
*
|
|
4
|
+
* The platform already owns this: `apps.icon_key` is artifact-level and never
|
|
5
|
+
* release-pinned, so a publisher refreshing their mark does not republish, and
|
|
6
|
+
* production falls back to the house robot for any agent that has none (see
|
|
7
|
+
* ArtifactIcon's `kind` branch). The CLI's job is only to show the same thing
|
|
8
|
+
* `terminus dev` will show in production, and to give an unpublished package a
|
|
9
|
+
* way to carry a mark until it has a remote to hold one.
|
|
10
|
+
*
|
|
11
|
+
* Hence two — and only two — places an icon can live:
|
|
12
|
+
*
|
|
13
|
+
* remote the linked artifact's own icon, read through the maintainer door.
|
|
14
|
+
* Authoritative once a package is linked: Studio and the dev are
|
|
15
|
+
* looking at one object, so an icon changed in either shows in both.
|
|
16
|
+
* staged `icon.png` (or .webp/.jpg) beside terminus.json. This exists ONLY
|
|
17
|
+
* to seed the mark of a package that has never been pushed, and
|
|
18
|
+
* `pushDraft` uploads it on the first push that finds the remote
|
|
19
|
+
* still bare. It never overwrites a mark set in Studio.
|
|
20
|
+
*
|
|
21
|
+
* With neither, the dev draws the robot — the same default production draws.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
|
|
27
|
+
/** The backend's own ceiling (routes/apps/media.rs MAX_ICON_BYTES). */
|
|
28
|
+
export const MAX_ICON_BYTES = 512 * 1024;
|
|
29
|
+
|
|
30
|
+
/** Sniffed, not trusted from the extension — the backend sniffs too, and a
|
|
31
|
+
* mislabelled file should fail here with a clear message rather than there. */
|
|
32
|
+
export function sniffImage(bytes) {
|
|
33
|
+
if (!bytes || bytes.length < 12) return null;
|
|
34
|
+
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
|
35
|
+
return { extension: "png", mediaType: "image/png" };
|
|
36
|
+
}
|
|
37
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
38
|
+
return { extension: "jpg", mediaType: "image/jpeg" };
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
bytes.toString("ascii", 0, 4) === "RIFF"
|
|
42
|
+
&& bytes.toString("ascii", 8, 12) === "WEBP"
|
|
43
|
+
) {
|
|
44
|
+
return { extension: "webp", mediaType: "image/webp" };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Candidate basenames, in the order a package would prefer them. */
|
|
50
|
+
export const STAGED_ICON_NAMES = ["icon.webp", "icon.png", "icon.jpg", "icon.jpeg"];
|
|
51
|
+
|
|
52
|
+
/** The staged mark beside terminus.json, or null. Reads the bytes because
|
|
53
|
+
* every caller needs them — to serve, to measure, or to upload. */
|
|
54
|
+
export async function readStagedIcon(dir) {
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = new Set(await readdir(dir));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
for (const name of STAGED_ICON_NAMES) {
|
|
62
|
+
if (!entries.has(name)) continue;
|
|
63
|
+
const absolute = path.join(dir, name);
|
|
64
|
+
let bytes;
|
|
65
|
+
try {
|
|
66
|
+
bytes = await readFile(absolute);
|
|
67
|
+
} catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const sniffed = sniffImage(bytes);
|
|
71
|
+
// A file called icon.png that is not an image is a mistake worth ignoring
|
|
72
|
+
// rather than failing the whole dev over.
|
|
73
|
+
if (!sniffed) continue;
|
|
74
|
+
return { name, absolute, bytes, mediaType: sniffed.mediaType };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `models` section of an agent's terminus.json.
|
|
3
|
+
*
|
|
4
|
+
* All model configuration lives under ONE key, because it is one decision:
|
|
5
|
+
*
|
|
6
|
+
* "models": {
|
|
7
|
+
* "default": "openai:gpt-5.6-luna",
|
|
8
|
+
* "mode": "all" | "selective" | "default", // absent means all
|
|
9
|
+
* "available": ["anthropic:claude-haiku-4-5"] // only under selective
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* An agent that says nothing gets the house configuration: every model on
|
|
13
|
+
* Terminus, starting on GPT-5.6 Luna. `terminus init agent` writes it out so
|
|
14
|
+
* it can be read and changed. `mode: "default"` narrows the agent to its
|
|
15
|
+
* default model alone.
|
|
16
|
+
*
|
|
17
|
+
* This is the only shape read: nothing reads any other key, so a key the
|
|
18
|
+
* section does not have changes nothing.
|
|
19
|
+
*
|
|
20
|
+
* The wire manifest still carries a top-level `model`, because that is a
|
|
21
|
+
* required, release-pinned field the backend validates — `models.default` is
|
|
22
|
+
* lifted onto it during the same normalization that turns tool sugar into
|
|
23
|
+
* capabilities. The section itself never reaches the backend; `agent_models`
|
|
24
|
+
* is a live setting on its own door, not part of a release.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const MODEL_MODES = ["default", "all", "selective"];
|
|
28
|
+
|
|
29
|
+
/** The model an agent starts on when terminus.json names none. */
|
|
30
|
+
export const DEFAULT_AGENT_MODEL = "openai:gpt-5.6-luna";
|
|
31
|
+
|
|
32
|
+
/** Which models an agent offers when terminus.json does not say. */
|
|
33
|
+
export const DEFAULT_MODEL_MODE = "all";
|
|
34
|
+
|
|
35
|
+
/** Read the section, filling in the house configuration where it is silent.
|
|
36
|
+
* Shape errors are the caller's to report. */
|
|
37
|
+
export function readModelSection(manifest = {}) {
|
|
38
|
+
const section = manifest.models && !Array.isArray(manifest.models) && typeof manifest.models === "object"
|
|
39
|
+
? manifest.models
|
|
40
|
+
: {};
|
|
41
|
+
const available = (Array.isArray(section.available) ? section.available : [])
|
|
42
|
+
.filter((entry) => typeof entry === "string")
|
|
43
|
+
.map((entry) => entry.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
const declared = String(section.mode ?? "").trim();
|
|
46
|
+
return {
|
|
47
|
+
default: String(section.default ?? "").trim() || DEFAULT_AGENT_MODEL,
|
|
48
|
+
mode: MODE_OF(declared, available),
|
|
49
|
+
available,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function MODE_OF(declared, available) {
|
|
54
|
+
if (MODEL_MODES.includes(declared)) return declared;
|
|
55
|
+
return available.length ? "selective" : DEFAULT_MODEL_MODE;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The section as terminus.json spells it: always a default and a mode, and
|
|
59
|
+
* a list only where `selective` gives it meaning. */
|
|
60
|
+
export function modelSectionDocument({ default: fallback, mode, available = [] }) {
|
|
61
|
+
const section = {
|
|
62
|
+
default: String(fallback ?? "").trim() || DEFAULT_AGENT_MODEL,
|
|
63
|
+
mode: MODEL_MODES.includes(mode) ? mode : DEFAULT_MODEL_MODE,
|
|
64
|
+
};
|
|
65
|
+
if (section.mode === "selective") {
|
|
66
|
+
if (available.length) section.available = [...available];
|
|
67
|
+
// A selection of nothing is the default model alone.
|
|
68
|
+
else section.mode = "default";
|
|
69
|
+
}
|
|
70
|
+
return section;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Write the section back into a manifest. */
|
|
74
|
+
export function writeModelSection(manifest, section) {
|
|
75
|
+
manifest.models = modelSectionDocument(section);
|
|
76
|
+
return manifest;
|
|
77
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An agent's `type` in terminus.json: who can read its conversations.
|
|
3
|
+
*
|
|
4
|
+
* "type": "observed"
|
|
5
|
+
*
|
|
6
|
+
* "default" — each conversation stays private to the person having it.
|
|
7
|
+
* "observed" — every conversation is shared with the agent's maintainers
|
|
8
|
+
* from the moment the visitor accepts the disclosure. It
|
|
9
|
+
* compiles to the collect plane's conversations channel, and
|
|
10
|
+
* that channel IS the collection: nothing else is declared.
|
|
11
|
+
*
|
|
12
|
+
* Omitting the key means "default", so the file never states it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { CliError } from "./client.mjs";
|
|
16
|
+
|
|
17
|
+
export const AGENT_TYPES = ["default", "observed"];
|
|
18
|
+
export const DEFAULT_AGENT_TYPE = "default";
|
|
19
|
+
|
|
20
|
+
/** The type terminus.json declares; anything it does not recognise reads as
|
|
21
|
+
* the default here, and the compile is what refuses it. */
|
|
22
|
+
export function readAgentType(manifest = {}) {
|
|
23
|
+
const type = String(manifest.type ?? "").trim();
|
|
24
|
+
return AGENT_TYPES.includes(type) ? type : DEFAULT_AGENT_TYPE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Set the type, writing the key only when it is not the default. A key that
|
|
28
|
+
* arrives sits beside `kind` — the other word for what this package is —
|
|
29
|
+
* rather than trailing after everything else. */
|
|
30
|
+
export function writeAgentType(manifest, type) {
|
|
31
|
+
if (!AGENT_TYPES.includes(type)) {
|
|
32
|
+
throw new CliError(`type is one of ${AGENT_TYPES.map((name) => `'${name}'`).join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (type === DEFAULT_AGENT_TYPE) {
|
|
35
|
+
delete manifest.type;
|
|
36
|
+
return manifest;
|
|
37
|
+
}
|
|
38
|
+
if (Object.hasOwn(manifest, "type") || !Object.hasOwn(manifest, "kind")) {
|
|
39
|
+
manifest.type = type;
|
|
40
|
+
return manifest;
|
|
41
|
+
}
|
|
42
|
+
// JSON keeps insertion order, so every key is re-seated with `type` after
|
|
43
|
+
// `kind`; the caller holds this object, which is why it is not replaced.
|
|
44
|
+
const entries = Object.entries(manifest);
|
|
45
|
+
for (const [key] of entries) delete manifest[key];
|
|
46
|
+
for (const [key, value] of entries) {
|
|
47
|
+
manifest[key] = value;
|
|
48
|
+
if (key === "kind") manifest.type = type;
|
|
49
|
+
}
|
|
50
|
+
return manifest;
|
|
51
|
+
}
|