@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -0
  3. package/bin/agent-discovery.mjs +71 -0
  4. package/bin/agent-icon.mjs +77 -0
  5. package/bin/agent-models.mjs +77 -0
  6. package/bin/agent-type.mjs +51 -0
  7. package/bin/agentdev.mjs +657 -0
  8. package/bin/app-route-script.mjs +59 -0
  9. package/bin/app-runtime-contract.mjs +2 -0
  10. package/bin/appdev-remote.mjs +346 -0
  11. package/bin/appdev.mjs +4446 -0
  12. package/bin/apps.mjs +5512 -0
  13. package/bin/capability-calls.mjs +437 -0
  14. package/bin/capsule-data.mjs +260 -0
  15. package/bin/client.mjs +189 -0
  16. package/bin/commands.mjs +1194 -0
  17. package/bin/dev-capsules.mjs +1599 -0
  18. package/bin/dev-contract.mjs +262 -0
  19. package/bin/dev-data.mjs +287 -0
  20. package/bin/dev-members.mjs +18 -0
  21. package/bin/dev-net.mjs +316 -0
  22. package/bin/dev-notification-popup.mjs +628 -0
  23. package/bin/dev-ports.mjs +567 -0
  24. package/bin/dev-server-binding.mjs +35 -0
  25. package/bin/dev-server-ops.mjs +1086 -0
  26. package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
  27. package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
  28. package/bin/dev-ui/OFL.txt +92 -0
  29. package/bin/dev-ui/agent-robot.webp +0 -0
  30. package/bin/dev-ui/app.js +5217 -0
  31. package/bin/dev-ui/highlight.js +195 -0
  32. package/bin/dev-ui/index.html +34 -0
  33. package/bin/dev-ui/style.css +3640 -0
  34. package/bin/devlint.mjs +112 -0
  35. package/bin/devserver.mjs +2127 -0
  36. package/bin/devtriggers.mjs +367 -0
  37. package/bin/endpoints.mjs +156 -0
  38. package/bin/errors.mjs +61 -0
  39. package/bin/files.mjs +169 -0
  40. package/bin/horizontal-capabilities/v1/contract.json +280 -0
  41. package/bin/http.mjs +500 -0
  42. package/bin/lint-manifests/justbash-commands.json +88 -0
  43. package/bin/lint-manifests/python-stdlib.json +295 -0
  44. package/bin/login-page.mjs +488 -0
  45. package/bin/schedules.mjs +664 -0
  46. package/bin/server-sandbox.mjs +204 -0
  47. package/bin/servicedev.mjs +425 -0
  48. package/bin/sync.mjs +357 -0
  49. package/bin/terminus.js +3666 -0
  50. package/bin/toolchain.mjs +125 -0
  51. package/bin/vendor/app-runtime-v1/app-host.json +124 -0
  52. package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
  53. package/bin/vendor/app-runtime-v1/doors.json +2867 -0
  54. package/bin/vendor/appd/node-harness.mjs +209 -0
  55. package/bin/vendor/appd/python-harness.py +12 -0
  56. package/bin/vendor/appd/server-protocol.json +84 -0
  57. package/bin/vendor/where.mjs +541 -0
  58. package/bin/versioning.mjs +72 -0
  59. package/bin/write-rules.mjs +398 -0
  60. 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
+ }