@kaiba-cloud/orbital 0.0.4 → 0.0.6

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/runner.js CHANGED
@@ -61666,7 +61666,7 @@ var MissionStateFileSchema = external_exports.object({
61666
61666
  source: external_exports.string().nullable(),
61667
61667
  result: external_exports.string().nullable(),
61668
61668
  question: external_exports.string().nullable(),
61669
- giver: external_exports.string().nullable(),
61669
+ receivers: external_exports.array(external_exports.string().min(1)).default([]),
61670
61670
  lead: external_exports.string().nullable(),
61671
61671
  claim: external_exports.string().nullable(),
61672
61672
  claimedAt: external_exports.string().nullable(),
@@ -61693,7 +61693,7 @@ var MissionRecordSchema = external_exports.object({
61693
61693
  result: external_exports.string().nullable(),
61694
61694
  question: external_exports.string().nullable(),
61695
61695
  questions: external_exports.array(MissionAskedSchema).default([]),
61696
- giver: external_exports.string().nullable(),
61696
+ receivers: external_exports.array(external_exports.string().min(1)).default([]),
61697
61697
  lead: external_exports.string().nullable(),
61698
61698
  lastActivity: external_exports.string().nullable().default(null),
61699
61699
  claim: external_exports.string().nullable().default(null),
@@ -61735,9 +61735,9 @@ var MissionReplySchema = external_exports.object({
61735
61735
  text: external_exports.string().min(1),
61736
61736
  questionId: external_exports.string().min(1).optional()
61737
61737
  });
61738
- var RemoteGiverSchema = external_exports.object({ node: external_exports.string().min(1), sessionId: external_exports.string().min(1) });
61738
+ var RemoteReceiverSchema = external_exports.object({ node: external_exports.string().min(1), sessionId: external_exports.string().min(1) });
61739
61739
  var SENDER_FIELDS = { team: external_exports.string().min(1).nullable().default(null), agent: external_exports.string().min(1).nullable().default(null) };
61740
- var RelayJoinSchema = external_exports.object({ missionId: MissionIdSchema, giver: RemoteGiverSchema, ...SENDER_FIELDS });
61740
+ var RelayJoinSchema = external_exports.object({ missionId: MissionIdSchema, receiver: RemoteReceiverSchema, ...SENDER_FIELDS });
61741
61741
  var RelayReplySchema = external_exports.object({ missionId: MissionIdSchema, text: external_exports.string().min(1), questionId: external_exports.string().min(1).optional(), by: external_exports.string().min(1), ...SENDER_FIELDS });
61742
61742
  var MissionControlSchema = external_exports.object({ missionId: MissionIdSchema });
61743
61743
  var SayRequestSchema = external_exports.object({
@@ -61758,11 +61758,12 @@ var MissionQuestionSchema = external_exports.object({
61758
61758
  leadSessionId: external_exports.string().min(1),
61759
61759
  question: external_exports.string(),
61760
61760
  questionId: external_exports.string().min(1).optional(),
61761
- giver: RemoteGiverSchema,
61761
+ receiver: RemoteReceiverSchema,
61762
61762
  team: external_exports.string().min(1).nullable().default(null)
61763
61763
  });
61764
61764
  var MissionJoinSchema = external_exports.object({
61765
- missionId: MissionIdSchema
61765
+ missionId: MissionIdSchema,
61766
+ externalId: external_exports.string().min(1).optional()
61766
61767
  });
61767
61768
 
61768
61769
  // src/protocol/charter.ts
@@ -61836,7 +61837,7 @@ var MissionStateEventSchema = external_exports.object({
61836
61837
  transition: MissionTransitionSchema,
61837
61838
  reason: external_exports.string().nullable(),
61838
61839
  by: external_exports.string(),
61839
- giver: external_exports.string().nullable(),
61840
+ receivers: external_exports.array(external_exports.string()).default([]),
61840
61841
  sessionId: external_exports.string().nullable(),
61841
61842
  lead: external_exports.string().nullable(),
61842
61843
  question: external_exports.string().nullable(),
@@ -61890,7 +61891,7 @@ var TelemetrySnapshotSchema = external_exports.object({
61890
61891
  state: MissionStateSchema,
61891
61892
  sessionId: external_exports.string().nullable(),
61892
61893
  lead: external_exports.string().nullable(),
61893
- giver: external_exports.string().nullable(),
61894
+ receivers: external_exports.array(external_exports.string()).default([]),
61894
61895
  givenBy: external_exports.string().nullable(),
61895
61896
  question: external_exports.string().nullable(),
61896
61897
  questions: external_exports.array(MissionAskedSchema).default([]),
@@ -62281,6 +62282,9 @@ var NodeClient = class {
62281
62282
  async joinMission(missionId) {
62282
62283
  return MissionRecordSchema.parse(await this.request("/orbital/missions/join", { method: "POST", body: { missionId } }));
62283
62284
  }
62285
+ async leaveMission(missionId) {
62286
+ return MissionRecordSchema.parse(await this.request("/orbital/missions/leave", { method: "POST", body: { missionId } }));
62287
+ }
62284
62288
  async handoff(handoff2) {
62285
62289
  return MissionRecordSchema.parse(await this.request("/orbital/missions/handoff", { method: "POST", body: handoff2 }));
62286
62290
  }
@@ -62517,6 +62521,13 @@ var DeliverySchema = external_exports.object({
62517
62521
  question: external_exports.string().nullable().default(null),
62518
62522
  text: external_exports.string()
62519
62523
  });
62524
+ var AnsweredSchema = external_exports.object({
62525
+ kind: external_exports.literal("answered"),
62526
+ mission: external_exports.string().min(1),
62527
+ question: external_exports.string().nullable(),
62528
+ by: external_exports.string().min(1),
62529
+ text: external_exports.string()
62530
+ });
62520
62531
  var SettleSchema = external_exports.object({
62521
62532
  taskId: external_exports.string().min(1).optional(),
62522
62533
  text: external_exports.string()
@@ -62668,6 +62679,60 @@ var LeaseSchema = external_exports.object({ holder: external_exports.string(), e
62668
62679
 
62669
62680
  // src/protocol/plugin-protocol.ts
62670
62681
  var PORTS_VERSION = "orbital/v1alpha1";
62682
+ var PLUGIN_COMMAND_RESERVED_OPTIONS = ["output", "quiet", "non-interactive", "confirm", "timeout", "help", "version", "workspace", "node"];
62683
+ var PluginCommandOptionSchema = external_exports.object({
62684
+ name: external_exports.string().regex(NAME_SHAPE).refine((name) => !PLUGIN_COMMAND_RESERVED_OPTIONS.includes(name), { error: `an option cannot take a name orbital gives every command: ${PLUGIN_COMMAND_RESERVED_OPTIONS.join(", ")}` }),
62685
+ type: external_exports.enum(["string", "boolean", "integer", "enum", "path"]),
62686
+ summary: external_exports.string().min(1),
62687
+ values: external_exports.array(external_exports.string().min(1)).min(1).optional(),
62688
+ required: external_exports.boolean().default(false),
62689
+ default: external_exports.union([external_exports.string(), external_exports.number().int(), external_exports.boolean()]).optional()
62690
+ }).strict().superRefine((option, issues) => {
62691
+ if (option.type === "enum" !== (option.values !== void 0)) issues.addIssue({ code: "custom", message: `the option ${option.name} names values only when its type is enum` });
62692
+ if (option.type === "boolean" && option.required) issues.addIssue({ code: "custom", message: `the boolean option ${option.name} cannot be required` });
62693
+ if (option.default === void 0) return;
62694
+ const fits = option.type === "boolean" ? typeof option.default === "boolean" : option.type === "integer" ? typeof option.default === "number" : typeof option.default === "string" && (option.values?.includes(option.default) ?? true);
62695
+ if (!fits) issues.addIssue({ code: "custom", message: `the default of the option ${option.name} is not a ${option.type === "enum" ? `value of ${option.values.join(", ")}` : option.type}` });
62696
+ });
62697
+ var PluginCommandArgSchema = external_exports.object({
62698
+ name: external_exports.string().regex(NAME_SHAPE),
62699
+ summary: external_exports.string().min(1),
62700
+ required: external_exports.boolean().default(false),
62701
+ variadic: external_exports.boolean().default(false)
62702
+ }).strict();
62703
+ var PluginCommandSchema = external_exports.object({
62704
+ name: external_exports.string().regex(NAME_SHAPE),
62705
+ summary: external_exports.string().min(1),
62706
+ args: external_exports.array(PluginCommandArgSchema).default([]),
62707
+ options: external_exports.array(PluginCommandOptionSchema).default([]),
62708
+ interactive: external_exports.boolean().default(false),
62709
+ node: external_exports.boolean().default(false)
62710
+ }).strict().superRefine((command, issues) => {
62711
+ const twice = (names) => names.find((name, index) => names.indexOf(name) !== index);
62712
+ const arg = twice(command.args.map((one) => one.name));
62713
+ if (arg) issues.addIssue({ code: "custom", message: `the command ${command.name} names the argument ${arg} twice` });
62714
+ const option = twice(command.options.map((one) => one.name));
62715
+ if (option) issues.addIssue({ code: "custom", message: `the command ${command.name} names the option ${option} twice` });
62716
+ if (command.args.slice(0, -1).some((one) => one.variadic)) issues.addIssue({ code: "custom", message: `only the last argument of the command ${command.name} can be variadic` });
62717
+ if (command.args.some((one, index) => one.required && command.args.slice(0, index).some((before) => !before.required))) issues.addIssue({ code: "custom", message: `a required argument of the command ${command.name} cannot come after an optional one` });
62718
+ if (command.interactive && command.node) issues.addIssue({ code: "custom", message: `the command ${command.name} cannot be interactive and need the node: the node has no terminal to give it` });
62719
+ });
62720
+ var PluginCommandsSchema = external_exports.array(PluginCommandSchema).default([]).superRefine((commands, issues) => {
62721
+ const names = commands.map((one) => one.name);
62722
+ const twice = names.find((name, index) => names.indexOf(name) !== index);
62723
+ if (twice) issues.addIssue({ code: "custom", message: `the command ${twice} is declared twice` });
62724
+ });
62725
+ var PluginCommandRequestSchema = external_exports.object({
62726
+ name: external_exports.string().regex(NAME_SHAPE),
62727
+ args: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.array(external_exports.string())])).default({}),
62728
+ options: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).default({}),
62729
+ terminal: external_exports.object({ tty: external_exports.boolean() }).nullable().default(null)
62730
+ }).strict();
62731
+ var PluginCommandResultSchema = external_exports.object({
62732
+ exitCode: external_exports.union([external_exports.literal(0), external_exports.literal(1), external_exports.literal(2), external_exports.literal(3), external_exports.literal(5)]).default(0),
62733
+ text: external_exports.string().nullable().default(null),
62734
+ data: external_exports.record(external_exports.string(), external_exports.unknown()).default({})
62735
+ }).strict();
62671
62736
  var PluginManifestSchema = external_exports.object({
62672
62737
  name: external_exports.string().regex(NAME_SHAPE),
62673
62738
  version: external_exports.string().min(1),
@@ -62697,7 +62762,7 @@ var PluginManifestSchema = external_exports.object({
62697
62762
  secrets: external_exports.array(external_exports.string().min(1)).default([]),
62698
62763
  models: external_exports.array(external_exports.string().regex(/^[a-z0-9-]+:.+$/, "a model pattern is <provider>:<model>, such as anthropic:*")).default([])
62699
62764
  }).prefault({}),
62700
- commands: external_exports.array(external_exports.object({ name: external_exports.string().regex(NAME_SHAPE), summary: external_exports.string() })).default([]),
62765
+ commands: PluginCommandsSchema,
62701
62766
  panels: external_exports.array(external_exports.object({ id: external_exports.string().min(1), title: external_exports.string(), kind: external_exports.enum(["list", "tree", "transcript", "form"]) })).default([])
62702
62767
  }).strict();
62703
62768
  var PluginInitSchema = external_exports.object({
@@ -62720,7 +62785,8 @@ var PluginPackageSchema = external_exports.object({
62720
62785
  dir: external_exports.string().min(1).optional(),
62721
62786
  env: external_exports.array(external_exports.string().regex(PLUGIN_ENV_NAME, "is not the name of an environment variable")).default([]),
62722
62787
  config: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
62723
- harnesses: external_exports.array(HarnessAdapterDeclarationSchema).default([])
62788
+ harnesses: external_exports.array(HarnessAdapterDeclarationSchema).default([]),
62789
+ commands: PluginCommandsSchema
62724
62790
  }).strict();
62725
62791
  var MarketplaceSchema = external_exports.object({
62726
62792
  name: external_exports.string().regex(NAME_SHAPE),
@@ -81712,14 +81778,14 @@ function buildMissionLinkTool(messaging, options) {
81712
81778
  };
81713
81779
  }
81714
81780
  var MissionAskParams = Type.Object({
81715
- question: Type.String({ minLength: 1, description: "The question, in full, with the options you see and what each one means: the giver has none of your context." })
81781
+ question: Type.String({ minLength: 1, description: "The question, in full, with the options you see and what each one means: the receivers have none of your context." })
81716
81782
  });
81717
81783
  function buildMissionAskTool(messaging, options) {
81718
81784
  return {
81719
81785
  name: "missionAsk",
81720
81786
  label: "Ask Mission Giver",
81721
- description: "Ask the giver of the mission of your session a question, as its lead, at any moment, also while your workers run. The node keeps each question open with the mission until the giver answers it, shows it to the person as waiting on them, and pushes it to the giver at once. The answer comes back to you as a message that names the question. Use it for every decision you need from the giver: a question only in your reply text reaches no one. Keep working on what does not depend on the answer.",
81722
- promptSnippet: "missionAsk: ask the giver of your mission a question at any moment, also while your workers run",
81787
+ description: "Ask the decision receivers of the mission of your session a question, as its lead, at any moment, also while your workers run. The node keeps each question open with the mission until a receiver answers it, shows it to the person as waiting on them, and pushes it at once to every live receiver. The answer comes back to you as a message that names the question. Use it for every decision you need: a question only in your reply text reaches no one. Keep working on what does not depend on the answer.",
81788
+ promptSnippet: "missionAsk: ask the decision receivers of your mission a question at any moment, also while your workers run",
81723
81789
  parameters: MissionAskParams,
81724
81790
  executionMode: "sequential",
81725
81791
  async execute(_toolCallId, params) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kaiba-cloud/orbital",
3
3
  "description": "orbital: the control plane lite for agents on any harness: the node, its runner, its terminal UI, its protocol and its Claude Code mod, in one command line",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "engines": {
@@ -11,17 +11,16 @@
11
11
  "orbital": "dist/orbital.js"
12
12
  },
13
13
  "exports": {
14
- ".": "./src/index.ts",
15
- "./protocol": "./src/protocol/index.ts",
16
- "./sdk": "./src/sdk/index.ts",
17
- "./tui": "./src/tui/index.tsx",
18
- "./*": "./src/*.ts"
14
+ ".": "./dist/index.js",
15
+ "./package.json": "./package.json"
19
16
  },
20
17
  "scripts": {
21
18
  "build": "node scripts/build.mjs",
22
19
  "check-types": "tsc -p tsconfig.json",
23
20
  "schemas": "node scripts/schemas.ts",
24
- "test": "vitest run"
21
+ "test": "vitest run",
22
+ "prepack": "node ../../scripts/pack-manifest.mjs prepack",
23
+ "postpack": "node ../../scripts/pack-manifest.mjs postpack"
25
24
  },
26
25
  "dependencies": {
27
26
  "@anthropic-ai/claude-agent-sdk": "^0.3.295",
@@ -566,11 +566,12 @@
566
566
  "by": {
567
567
  "type": "string"
568
568
  },
569
- "giver": {
570
- "type": [
571
- "string",
572
- "null"
573
- ]
569
+ "receivers": {
570
+ "default": [],
571
+ "type": "array",
572
+ "items": {
573
+ "type": "string"
574
+ }
574
575
  },
575
576
  "sessionId": {
576
577
  "type": [
@@ -611,7 +612,7 @@
611
612
  "transition",
612
613
  "reason",
613
614
  "by",
614
- "giver",
615
+ "receivers",
615
616
  "sessionId",
616
617
  "lead",
617
618
  "question",
@@ -279,11 +279,12 @@
279
279
  "null"
280
280
  ]
281
281
  },
282
- "giver": {
283
- "type": [
284
- "string",
285
- "null"
286
- ]
282
+ "receivers": {
283
+ "default": [],
284
+ "type": "array",
285
+ "items": {
286
+ "type": "string"
287
+ }
287
288
  },
288
289
  "givenBy": {
289
290
  "type": [
@@ -342,7 +343,7 @@
342
343
  "state",
343
344
  "sessionId",
344
345
  "lead",
345
- "giver",
346
+ "receivers",
346
347
  "givenBy",
347
348
  "question",
348
349
  "questions",
@@ -37,7 +37,7 @@ plain object for each port.
37
37
  | workflows | `ports.workflows` | run workflow files on an engine | [workflows](references/ports.md#workflows) |
38
38
  | reasoner | `ports.reasoner` | reason over an OWL ontology | [reasoner](references/ports.md#reasoner) |
39
39
  | missions | `ports.missions` | join, say, reply, pause, resume and ask on a mission of another node | [missions](references/ports.md#missions) |
40
- | commands | `commands` (beside `ports`) | `orbital plugin run <plugin> <command>` | [commands](references/ports.md#commands) |
40
+ | commands | `commands` (beside `ports`), and `commands` in `orbital-plugin.json` | `orbital <plugin> <command>`, such as a login | [commands](references/ports.md#commands) |
41
41
  | service | `service` (beside `ports`) | background work while the node serves | [service](references/ports.md#service) |
42
42
 
43
43
  ## Workflow
@@ -126,7 +126,10 @@ orbital plugin check <name> --fixtures <name>/fixtures.json
126
126
 
127
127
  `plugin add` prints the permissions, and names a folder in `orbital.yaml` by its
128
128
  path. It does not copy it. Rebuild, and the next start of the node uses the new
129
- build. `plugin check` starts the built plugin with the config of its
129
+ build. A plugin that checks its config when it starts gets it at the add:
130
+ `orbital plugin add ./<name> --config channel=general`, or `--config-file <file>`.
131
+ The add starts the plugin with that config, and writes it to `orbital.yaml` only
132
+ when the plugin starts. `plugin check` starts the built plugin with the config of its
130
133
  `orbital.yaml` entry and the secrets of `orbital plugin secret set`.
131
134
 
132
135
  ### 5. Add to the config
@@ -148,9 +151,9 @@ how. Then `orbital node start`.
148
151
 
149
152
  ### 6. Publish
150
153
 
151
- - **npm.** Publish `orbital-plugin.json` and `dist/`. Name the package so that
152
- its last part is the plugin's name, such as `@acme/discord`.
153
- `orbital plugin add npm:@acme/discord` names the plugin by that part.
154
+ - **npm.** Publish `orbital-plugin.json` and `dist/`. The package can have any
155
+ name: `orbital plugin add npm:@acme/orbital-discord` names the plugin by the
156
+ `name` of its `orbital-plugin.json`, such as `discord`.
154
157
  - **A marketplace.** A Git repository with a `marketplace.json`. A person adds
155
158
  it with `orbital plugin marketplace add <git-url>`, then runs
156
159
  `orbital plugin add discord@<marketplace>`.
@@ -43,7 +43,7 @@ message. Catch it where a failure must not stop the plugin.
43
43
  | `sayMission(request)` | `Said` | Far side of `missions.say`. |
44
44
  | `pauseMission(request)` | `MissionRecord` | Far side of `missions.pause`. |
45
45
  | `resumeMission(request)` | `MissionRecord` | Far side of `missions.resume`. |
46
- | `askGiver(question)` | `boolean` | Far side of `missions.ask`. |
46
+ | `askReceiver(question)` | `boolean` | Far side of `missions.ask`. |
47
47
  | `snapshot()` | `TelemetrySnapshot` | Read the telemetry graph of the node now. |
48
48
 
49
49
  ### submit
@@ -199,26 +199,26 @@ interface MissionStarted { missionId: string; sessionId: string; taskId: string;
199
199
  ### missions
200
200
 
201
201
  `missions()` answers the missions that this node holds, with their state,
202
- lead, question, giver and history. It never includes the missions of other
202
+ lead, question, decision receivers and history. It never includes the missions of other
203
203
  nodes, so a relay can ask each node which missions it holds.
204
204
 
205
- ### joinMission, replyMission, sayMission, pauseMission, resumeMission, askGiver
205
+ ### joinMission, replyMission, sayMission, pauseMission, resumeMission, askReceiver
206
206
 
207
207
  These are the far side of the `missions` port. A relay plugin on the node that
208
208
  holds the mission calls them when the same plugin on another node sends it a
209
209
  request. See [ports.md](ports.md#missions).
210
210
 
211
211
  ```ts
212
- interface RelayJoin { missionId: string; giver: { node: string; sessionId: string }; team: string | null; agent: string | null }
212
+ interface RelayJoin { missionId: string; receiver: { node: string; sessionId: string }; team: string | null; agent: string | null }
213
213
  interface RelayReply { missionId: string; text: string; by: string; team: string | null; agent: string | null }
214
214
  interface RelayControl { missionId: string; by: string; team: string | null; agent: string | null }
215
215
  interface RelayMessage { missionId: string; text: string; by: string; team: string | null; agent: string | null }
216
216
  interface Said { sessionId: string; missionId: string | null; delivery: 'reply' | 'steer' | 'turn' }
217
- interface MissionQuestion { missionId: string; node: string; lead: string | null; leadSessionId: string; question: string; giver: { node: string; sessionId: string }; team: string | null }
217
+ interface MissionQuestion { missionId: string; node: string; lead: string | null; leadSessionId: string; question: string; receiver: { node: string; sessionId: string }; team: string | null }
218
218
  ```
219
219
 
220
- - `joinMission` makes the remote session the giver of the mission. When the
221
- mission waits, the node sends the question to the new giver at once.
220
+ - `joinMission` adds the remote session to the decision receivers of the
221
+ mission. The node sends each open question to the new receiver at once.
222
222
  - `replyMission` answers a waiting mission. It fails when the mission is
223
223
  paused, or is not waiting.
224
224
  - `sayMission` gives the words to the lead of the mission. `delivery` says
@@ -227,8 +227,8 @@ interface MissionQuestion { missionId: string; node: string; lead: string | null
227
227
  lead.
228
228
  - `pauseMission` pauses an active or waiting mission. `resumeMission` resumes a
229
229
  paused one.
230
- - `askGiver` gives the question to the giver session on this node. It answers
231
- false when the giver is not an attached session of this node.
230
+ - `askReceiver` gives the question to the receiver session on this node. It
231
+ answers false when the receiver is not a live session of this node.
232
232
  - `team` and `agent` name the team and the agent of the session that sent the
233
233
  request, or are null. In a question, `team` is the team of the mission and
234
234
  `lead` is the agent. The node gives them to the `channel.receive` policy
@@ -243,7 +243,7 @@ interface MissionQuestion { missionId: string; node: string; lead: string | null
243
243
  interface TelemetrySnapshot {
244
244
  v: 'orbital.telemetry/v1'; node: string; sequence: number; at: string; startedAt: string;
245
245
  teams: Array<{ name: string; node: string; purpose: string | null; lead: string | null; members: Array<{ agent: string; role: 'lead' | 'member'; responsibility: string | null; harness: string | null; placement: string | null; state: 'working' | 'idle'; lastActivityAt: string | null; work: MemberWork[] }> }>;
246
- missions: Array<{ missionId: string; team: string | null; state: MissionState; sessionId: string | null; lead: string | null; giver: string | null; givenBy: string | null; question: string | null; result: string | null; updatedAt: string | null }>;
246
+ missions: Array<{ missionId: string; team: string | null; state: MissionState; sessionId: string | null; lead: string | null; receivers: string[]; givenBy: string | null; question: string | null; result: string | null; updatedAt: string | null }>;
247
247
  sessions: Array<{ sessionId: string; parentSessionId: string | null; agent: string; harness: string; team: string | null; mission: string | null; role: string; placement: string; running: boolean; since: string | null }>;
248
248
  jobs: Array<{ jobId: string; sessionId: string; parentSessionId: string | null; agent: string; harness: string; team: string | null; mission: string | null; since: string }>;
249
249
  tokens: Array<{ team: string | null; mission: string | null; provider: string; model: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; costUsd: number; llmCalls: number; turns: number }>;
@@ -16,7 +16,8 @@ A plugin has two descriptions:
16
16
  "command": ["node", "dist/plugin.js"],
17
17
  "dir": ".",
18
18
  "env": ["LAN_SHARED_KEY"],
19
- "config": { "port": 7420 }
19
+ "config": { "port": 7420 },
20
+ "commands": [{ "name": "status", "summary": "Show the peers." }]
20
21
  }
21
22
  ```
22
23
 
@@ -27,6 +28,7 @@ A plugin has two descriptions:
27
28
  | `dir` | no | The working folder, relative to the plugin's folder. Default: the plugin's folder. |
28
29
  | `env` | no | Environment variables the node passes to the plugin. It must list the same names as the secrets in the manifest that have the form of a variable (`[A-Z][A-Z0-9_]*`). |
29
30
  | `config` | no | Default config. The `config` of the `orbital.yaml` entry overrides it key by key. |
31
+ | `commands` | no | The CLI commands of the plugin, the same as its `commands` in code. See [commands](ports.md#commands). |
30
32
 
31
33
  No other field is allowed. A `orbital.yaml` source `exec:<command> <args>`
32
34
  starts a program on PATH with no `orbital-plugin.json`. Only such an entry can
@@ -49,6 +51,9 @@ plugins:
49
51
 
50
52
  `orbital plugin add <source>` writes the entry. A folder source stays where it
51
53
  is. Any other source is installed in `<orbital home>/plugins/<name>`.
54
+ `--config key=value` (repeatable) and `--config-file <file>` give the config:
55
+ the add starts the plugin with it, and writes the entry only when the start
56
+ succeeds.
52
57
 
53
58
  ## The manifest
54
59
 
@@ -75,7 +80,7 @@ interface PluginManifest {
75
80
  missions: boolean;
76
81
  };
77
82
  permissions: { network: string[]; secrets: string[]; models: string[] };
78
- commands: Array<{ name: string; summary: string }>;
83
+ commands: Array<{ name: string; summary: string; args: PluginCommandArg[]; options: PluginCommandOption[]; interactive: boolean; node: boolean }>;
79
84
  panels: Array<{ id: string; title: string; kind: 'list' | 'tree' | 'transcript' | 'form' }>;
80
85
  }
81
86
  ```
@@ -227,7 +227,7 @@ reason to the sender:
227
227
  | Path | The sender gets |
228
228
  |---|---|
229
229
  | `host.deliver`: work, a cancel, the answer of a person to a question | `DeliveryAnswer` with `accepted: false` and the `reason`. For refused work, the node also sends a reply over the same channel (see below) |
230
- | `host.joinMission`, `replyMission`, `sayMission`, `pauseMission`, `resumeMission`, `askGiver` | an error with the reason |
230
+ | `host.joinMission`, `replyMission`, `sayMission`, `pauseMission`, `resumeMission`, `askReceiver` | an error with the reason |
231
231
  | `host.a2a`, and a request to `/a2a` from a non-loopback address | a JSON-RPC error with the reason |
232
232
 
233
233
  The answer of a remote node to a request of this node is not checked: this
@@ -1007,8 +1007,8 @@ LAN or cross-machine plugin implements.
1007
1007
  itself. It asks each plugin with the port, in plugin order. The first answer
1008
1008
  that is not null (or true) wins.
1009
1009
 
1010
- - `join(request)`: an attached session on this node joins a mission as its
1011
- giver (`missionJoin`). `request.giver` is `{ node, sessionId }` of this node.
1010
+ - `join(request)`: a session on this node joins a mission as a decision
1011
+ receiver (`missionJoin`). `request.receiver` is `{ node, sessionId }` of this node.
1012
1012
  - `say(request)`: a person or a session talks to the lead of a mission
1013
1013
  (`mission say`, `missionSay`). The far node gives the words to the lead: in
1014
1014
  its running turn, as its next turn, or as the answer to its question. When
@@ -1017,7 +1017,7 @@ that is not null (or true) wins.
1017
1017
  mission (`mission reply`, `missionReply`).
1018
1018
  - `pause(request)`, `resume(request)`: `mission pause`, `mission resume`.
1019
1019
  - `ask(question)`: the lead of a mission on this node waits with a question,
1020
- and the giver is a session of another node. Answer true when you carried the
1020
+ and a decision receiver is a session of another node. Answer true when you carried the
1021
1021
  question to that node. When no plugin answers true, the node warns.
1022
1022
 
1023
1023
  Answer null (or false) when you do not know the mission or the node. Else
@@ -1033,7 +1033,7 @@ transport, then calls the host API of its node:
1033
1033
  | `reply(request)` | `host.replyMission(request)` |
1034
1034
  | `pause(request)` | `host.pauseMission(request)` |
1035
1035
  | `resume(request)` | `host.resumeMission(request)` |
1036
- | `ask(question)` | `host.askGiver(question)` (true when delivered) |
1036
+ | `ask(question)` | `host.askReceiver(question)` (true when delivered) |
1037
1037
 
1038
1038
  The near node fills `team` and `agent` with the team and the agent of the
1039
1039
  session that sends the request, or null. In a question, `team` is the team of
@@ -1058,18 +1058,18 @@ interface MissionRelayPort {
1058
1058
  ask(question: MissionQuestion): Promise<boolean>;
1059
1059
  say(request: RelayMessage): Promise<Said | null>;
1060
1060
  }
1061
- interface RemoteGiver { node: string; sessionId: string }
1062
- interface RelayJoin { missionId: string; giver: RemoteGiver; team: string | null; agent: string | null }
1061
+ interface RemoteReceiver { node: string; sessionId: string }
1062
+ interface RelayJoin { missionId: string; receiver: RemoteReceiver; team: string | null; agent: string | null }
1063
1063
  interface RelayReply { missionId: string; text: string; by: string; team: string | null; agent: string | null }
1064
1064
  interface RelayControl { missionId: string; by: string; team: string | null; agent: string | null }
1065
1065
  interface RelayMessage { missionId: string; text: string; by: string; team: string | null; agent: string | null }
1066
1066
  interface Said { sessionId: string; missionId: string | null; delivery: 'reply' | 'steer' | 'turn' }
1067
- interface MissionQuestion { missionId: string; node: string; lead: string | null; leadSessionId: string; question: string; giver: RemoteGiver; team: string | null }
1067
+ interface MissionQuestion { missionId: string; node: string; lead: string | null; leadSessionId: string; question: string; receiver: RemoteReceiver; team: string | null }
1068
1068
  interface MissionRecord {
1069
1069
  missionId: string; team: string | null; node: string; file: string | null;
1070
1070
  state: 'open' | 'active' | 'waiting' | 'paused' | 'done' | 'failed';
1071
1071
  declared: boolean; givenBy: string | null; brief: string | null; sessionId: string | null; taskId: string | null;
1072
- source: string | null; result: string | null; question: string | null; giver: string | null; lead: string | null;
1072
+ source: string | null; result: string | null; question: string | null; receivers: string[]; lead: string | null;
1073
1073
  lastActivity: string | null; claim: string | null; claimedAt: string | null; stale: boolean; updatedAt: string | null;
1074
1074
  history: Array<{ state: MissionState; at: string; by: string; note: string | null }>;
1075
1075
  }
@@ -1090,7 +1090,7 @@ ports: {
1090
1090
  reply: async (request) => peer.ask('reply', request),
1091
1091
  pause: async (request) => peer.ask('pause', request),
1092
1092
  resume: async (request) => peer.ask('resume', request),
1093
- ask: async (question) => (await peer.send(question.giver.node, 'ask', question)) === true,
1093
+ ask: async (question) => (await peer.send(question.receiver.node, 'ask', question)) === true,
1094
1094
  say: async (request) => peer.ask('say', request),
1095
1095
  },
1096
1096
  },
@@ -1103,19 +1103,72 @@ test it with two nodes.
1103
1103
 
1104
1104
  ## `commands`
1105
1105
 
1106
- `commands` sits beside `ports`, not in it. A person runs a command with
1107
- `orbital plugin run <plugin> <command> -- <args>`. The node prints the answer.
1106
+ `commands` sits beside `ports`, not in it. Each command adds a command to the
1107
+ CLI: a person runs `orbital <plugin> <command> [args] [--options]`.
1108
+ `orbital help` and `orbital <plugin> --help` list the commands.
1109
+
1110
+ Declare each command two times, with the same fields:
1111
+
1112
+ - In `orbital-plugin.json`, so that orbital lists the command without a start
1113
+ of the plugin.
1114
+ - In the code, with `defineCommand`. The SDK types `args` and `options` from
1115
+ the declaration.
1116
+
1117
+ The node refuses a plugin when the two lists are not the same.
1118
+
1119
+ ```json
1120
+ { "name": "acme", "command": ["node", "dist/plugin.js"],
1121
+ "commands": [
1122
+ { "name": "login", "summary": "Sign in to acme in the browser.", "interactive": true },
1123
+ { "name": "status", "summary": "Show the account.", "options": [{ "name": "verbose", "type": "boolean", "summary": "Show more." }] }
1124
+ ] }
1125
+ ```
1108
1126
 
1109
1127
  ```ts
1110
- commands: {
1111
- whoami: {
1112
- summary: 'Name the account this plugin posts as.',
1113
- run: async (args) => `bot in ${args[0] ?? 'the default guild'}`,
1114
- },
1115
- },
1128
+ import { defineCommand } from '@kaiba-cloud/orbital-sdk';
1129
+
1130
+ commands: [
1131
+ defineCommand({
1132
+ name: 'login',
1133
+ summary: 'Sign in to acme in the browser.',
1134
+ interactive: true,
1135
+ run: async ({ terminal }) => {
1136
+ terminal!.output.write(`Open ${url} and type the code.\n`);
1137
+ return { text: 'Signed in.', data: { account } };
1138
+ },
1139
+ }),
1140
+ defineCommand({
1141
+ name: 'status',
1142
+ summary: 'Show the account.',
1143
+ options: { verbose: { type: 'boolean', summary: 'Show more.' } },
1144
+ run: async ({ options }) => ({ text: options.verbose ? detail : account, data: { account } }),
1145
+ }),
1146
+ ],
1116
1147
  ```
1117
1148
 
1118
- **Remote method.** `command.run {name, args}`, answered with `{ output }`.
1149
+ | Field | Meaning |
1150
+ |---|---|
1151
+ | `name`, `summary` | The command word and its line in the help. A plugin whose name is a built-in command, such as `plugin` or `check`, gets no command. |
1152
+ | `args` | Positional arguments: `name`, `summary`, `required`, `variadic` (the last only). |
1153
+ | `options` | Flags: `name`, `type` (`string`, `boolean`, `integer`, `enum`, `path`), `summary`, `values` (enum only), `required`, `default`. `output`, `quiet`, `non-interactive`, `confirm`, `timeout`, `help`, `version`, `workspace` and `node` belong to orbital. |
1154
+ | `interactive` | The command needs a terminal, such as a browser or device login. `terminal` gives it the person's stdin, stdout and stderr. With `--output json`, its stdout goes to stderr. Without a terminal, orbital refuses the command. |
1155
+ | `node` | The command runs in the plugin of the running node, not in a new process. Without a node, orbital refuses it. It cannot be interactive. |
1156
+
1157
+ orbital starts the plugin for the command only, with its config from
1158
+ `orbital.yaml` and the workspace in `context.init`. No node needs to run.
1159
+
1160
+ `run` answers `{ text?, data?, exitCode? }`, or a string as `text`. Text mode
1161
+ prints `text`. `--output json` prints `data` in the envelope. `exitCode` is 0,
1162
+ 1, 2 (no credential), 3 (bad input) or 5, and orbital exits with it.
1163
+
1164
+ In an interactive command, write to `terminal.output`, never to
1165
+ `process.stdout`: stdout carries the protocol.
1166
+
1167
+ The commands are for a person at the CLI. Agents do not see them: give agents
1168
+ a tool with `ports.tools`.
1169
+
1170
+ **Remote method.** `command.run {name, args, options, terminal}`, answered
1171
+ with `{ exitCode, text, data }`.
1119
1172
 
1120
1173
  ## `service`
1121
1174
 
@@ -18,7 +18,7 @@ key. Then:
18
18
  - the agents of each node list the teams and agents of the other nodes
19
19
  (`directory`);
20
20
  - a person on one node joins, talks to, answers, pauses and resumes a mission
21
- whose lead runs on another node, and the lead's question reaches the giver on its own
21
+ whose lead runs on another node, and the lead's question reaches each decision receiver on its own
22
22
  node (`missions`);
23
23
  - `orbital mission list` and the claims see the missions of every node, and
24
24
  `orbital up` shows the peers (`panels`).
@@ -130,7 +130,7 @@ export function plugin(context: PluginContext): PluginImplementation {
130
130
  }
131
131
  const body = await read(request);
132
132
  if (path === '/a2a') return a2a(host, body, response);
133
- if (path === '/missions/ask') return send(response, 200, { delivered: await host.askGiver(JSON.parse(body)) });
133
+ if (path === '/missions/ask') return send(response, 200, { delivered: await host.askReceiver(JSON.parse(body)) });
134
134
  const relay = /^\/missions\/(join|say|reply|pause|resume)$/.exec(path);
135
135
  if (!relay) return send(response, 404, { error: `No route ${path}.` });
136
136
  const asked = JSON.parse(body);
@@ -184,7 +184,7 @@ export function plugin(context: PluginContext): PluginImplementation {
184
184
  pause: async (request) => firstAnswer<MissionRecord>('/missions/pause', request),
185
185
  resume: async (request) => firstAnswer<MissionRecord>('/missions/resume', request),
186
186
  ask: async (question) => {
187
- const peer = peers.find((one) => one.name === question.giver.node);
187
+ const peer = peers.find((one) => one.name === question.receiver.node);
188
188
  if (!peer) return false;
189
189
  return (await call<{ delivered: boolean }>(peer, '/missions/ask', question).catch(() => null))?.delivered === true;
190
190
  },
@@ -236,7 +236,7 @@ How it works:
236
236
  its node. See the table in [ports.md](ports.md#missions).
237
237
  - **Missions.** `join`, `say`, `reply`, `pause` and `resume` ask each peer. A peer
238
238
  answers 404 when `host.missions()` does not hold the mission, and the next
239
- peer is asked. `ask` goes to the peer that `question.giver.node` names.
239
+ peer is asked. `ask` goes to the peer that `question.receiver.node` names.
240
240
  - **Placements.** The node of the far agent never shows its own secret. The
241
241
  plugin serves A2A at `/a2a` and passes each call to `host.a2a`. The host API
242
242
  does not stream, so the plugin answers `SubscribeToTask` as a stream of