@huynle/hub 0.8.7 → 0.9.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/CHANGELOG.md +129 -0
- package/README.md +69 -12
- package/package.json +5 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +278 -35
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +23 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/launches.ts +5 -1
- package/packages/cli/src/mcp.ts +28 -8
- package/packages/cli/src/messaging.ts +14 -0
- package/packages/cli/src/team-intake-mcp.ts +26 -2
- package/packages/codex/package.json +1 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +677 -0
- package/packages/protocol/src/artifacts.ts +43 -0
- package/packages/protocol/src/credentials.ts +38 -0
- package/packages/protocol/src/index.ts +56 -5
- package/packages/protocol/src/launch.ts +7 -2
- package/packages/protocol/src/markdown.ts +753 -0
- package/packages/protocol/src/media-allowlist.ts +240 -0
- package/packages/protocol/src/media.ts +120 -0
- package/packages/protocol/src/preview-bundle.ts +221 -0
- package/packages/protocol/src/voice-recording.ts +8 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/client.ts +17 -3
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/team-intake.ts +8 -0
- package/packages/sdk/src/types.ts +16 -2
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/agent/delivery.ts +35 -6
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/events.ts +11 -2
- package/src/central/agent/launch-engine.ts +65 -4
- package/src/central/agent/launch-routes.ts +8 -0
- package/src/central/agent/launch-spec.ts +18 -1
- package/src/central/agent/launch-store.ts +2 -2
- package/src/central/agent/message-routes.ts +49 -11
- package/src/central/agent/messages.ts +17 -4
- package/src/central/agent/not-after.ts +28 -0
- package/src/central/agent/placement.ts +36 -4
- package/src/central/agent/routes.ts +3 -1
- package/src/central/agent/webhook-routes.ts +21 -9
- package/src/central/agent/webhooks.ts +27 -9
- package/src/central/artifacts/host.ts +3 -1
- package/src/central/artifacts/maintenance.ts +2 -1
- package/src/central/artifacts/preview-bundle.ts +81 -0
- package/src/central/artifacts/previews.ts +94 -0
- package/src/central/artifacts/routes.ts +12 -1
- package/src/central/artifacts/store.ts +198 -8
- package/src/central/config.ts +34 -0
- package/src/central/fetch-budget.ts +59 -0
- package/src/central/fetcher.ts +372 -0
- package/src/central/http.ts +277 -22
- package/src/central/media-remote.ts +248 -0
- package/src/central/media-timeline.ts +317 -0
- package/src/central/media-unfurl.ts +119 -0
- package/src/central/profiles/content.ts +3 -35
- package/src/central/recording/routes.ts +2 -2
- package/src/central/recording/schema.ts +3 -1
- package/src/central/registry.ts +449 -21
- package/src/central/spoke-ws.ts +17 -3
- package/src/central/store.ts +21 -0
- package/src/central/teams/intake-routes.ts +2 -1
- package/src/central/teams/intake.ts +200 -15
- package/src/central/unfurl-parse.ts +313 -0
- package/src/index.ts +68 -8
- package/src/speakable.ts +33 -1
- package/src/spoke/conversation.ts +7 -4
- package/src/spoke/deliver.ts +19 -0
- package/src/spoke/media-bytes.ts +171 -0
- package/src/spoke/media-policy.ts +394 -0
- package/src/spoke/media.ts +189 -0
- package/src/spoke/own-server.ts +30 -0
- package/src/spoke/preview-bundle.ts +538 -0
- package/src/spoke/spoke.ts +387 -17
- package/src/spoke/timeline.ts +59 -5
- package/src/spoke/voice-front.ts +335 -0
- package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
- package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
- package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
- package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
- package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
- package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
- package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
- package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
- package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
- package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
- package/web/dist/assets/ArtifactPage-4GoBqSpU.js +1 -0
- package/web/dist/assets/ArtifactPane-Dx_0iWwH.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-CuHgARMk.js +6 -0
- package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
- package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
- package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
- package/web/dist/assets/Diagram-CZM_R7MG.js +1 -0
- package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
- package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
- package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
- package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
- package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
- package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
- package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
- package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
- package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
- package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
- package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
- package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
- package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
- package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
- package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
- package/web/dist/assets/Library-TwvrwqSk.js +2 -0
- package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
- package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
- package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
- package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
- package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
- package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
- package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
- package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
- package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
- package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
- package/web/dist/assets/ProfilesDestination-rKNDpLjR.js +1 -0
- package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
- package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
- package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
- package/web/dist/assets/TeamsPage-DtJylLrt.js +1 -0
- package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
- package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
- package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
- package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
- package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
- package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
- package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
- package/web/dist/assets/Watchtower-Bmgdyfjb.js +2 -0
- package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
- package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
- package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
- package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
- package/web/dist/assets/index-C3ci5UtY.css +1 -0
- package/web/dist/assets/index-D-xG9nrc.js +36 -0
- package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
- package/web/dist/assets/live-7q72Lyj2.js +5 -0
- package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
- package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
- package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-hfZxM6Xl.js} +1 -1
- package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-Km9vipTM.js} +1 -1
- package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-BAbgBiFk.js} +1 -1
- package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
- package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
- package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
- package/web/dist/assets/source-BWlvaODa.js +1 -0
- package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
- package/web/dist/assets/ui-CFB4wQfF.js +3 -0
- package/web/dist/index.html +8 -6
- package/web/src/voice/turn/LICENSE +28 -0
- package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
- package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
- package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
- package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
- package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
- package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
- package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
- package/web/dist/assets/hub-CrbItiwP.js +0 -13
- package/web/dist/assets/index-82Ce0IAf.css +0 -1
- package/web/dist/assets/index-D9SP3_0z.js +0 -22
- package/web/dist/assets/live-BNJgy0vg.js +0 -5
- package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
- package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
- package/web/dist/assets/source-Wbq35qGp.js +0 -1
- package/web/dist/assets/ui-j5FFk90B.js +0 -3
package/packages/cli/src/cli.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { readSpokeCredentials } from "../../node/src/config.ts"
|
|
|
14
14
|
import { hostname } from "node:os"
|
|
15
15
|
import { serveMcp } from "./mcp"
|
|
16
16
|
import { intentLine, writeIntent } from "./intents"
|
|
17
|
-
import { ASK_EXPIRES_DEFAULT_S, askOutcome, declinedText, durationArg, inboxText, mutedText, repliedText, sentText, textArg } from "./messaging"
|
|
17
|
+
import { ASK_EXPIRES_DEFAULT_S, askOutcome, declinedText, durationArg, inboxText, mutedText, notAfterArg, repliedText, sentText, textArg } from "./messaging"
|
|
18
18
|
import { watch } from "./watch"
|
|
19
19
|
import { DECISION_FLAGS, decisionOptions, decisionsText, openedText, stateArg } from "./decisions"
|
|
20
20
|
import { GRANT_FLAGS, GRANT_REQUEST_FLAGS, grantError, grantRequestArg, grantSpecArg, grantsText, grantText, requestedText, userOnly } from "./grants"
|
|
@@ -81,10 +81,13 @@ Identity
|
|
|
81
81
|
hub role release <role[.project]> [--session <addr>]
|
|
82
82
|
|
|
83
83
|
Messaging (text "-" reads it from stdin)
|
|
84
|
-
hub msg <addr> "text" [--all] [--no-wake] [--urgent]
|
|
85
|
-
|
|
84
|
+
hub msg <addr> "text" [--all] [--no-wake] [--urgent] [--not-after <ms|ISO time>] [--key K]
|
|
85
|
+
tell: fire and forget; --not-after: never delivered after that time
|
|
86
|
+
hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--not-after <ms|ISO time>] [--all] [--no-wake] [--urgent] [--key K]
|
|
86
87
|
tracked until answered, declined or expired; --wait alone waits until
|
|
87
|
-
it expires; exit 0 answered, 1 declined or no answer yet, 8 expired
|
|
88
|
+
it expires; exit 0 answered, 1 declined or no answer yet, 8 expired;
|
|
89
|
+
--key (Idempotency-Key): a retry with the same key and text answers
|
|
90
|
+
the first send again and sends nothing (24 h, per credential)
|
|
88
91
|
hub reply <msgID> "text" [--no-wake] goes to the real sender
|
|
89
92
|
hub decline <msgID> --why "reason"
|
|
90
93
|
hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…"]] [--urgency low|normal|high]
|
|
@@ -119,9 +122,10 @@ Launching (users; agents and app tokens with sessions:launch under a grant with
|
|
|
119
122
|
[--name <@name>] [--role R]... [--harness H] [--agent A] [--model provider/model]
|
|
120
123
|
[--runtime host|openshell [--profile P]] [--machine M] [--node-label k=v]... [--prefer]
|
|
121
124
|
[--label k=v]... [--prompt "text" | --prompt -] [--end idle|asked|never] [--timeout 4h]
|
|
122
|
-
[--cleanup keep|remove] [--key K]
|
|
125
|
+
[--cleanup keep|remove] [--key K] [--not-after <ms|ISO time>]
|
|
123
126
|
start a session; the same --key again answers the same launch;
|
|
124
|
-
--label tags it for apps, --node-label picks nodes (--prefer: not required)
|
|
127
|
+
--label tags it for apps, --node-label picks nodes (--prefer: not required);
|
|
128
|
+
--not-after: still waiting for a machine then, it never starts (expired)
|
|
125
129
|
hub launches list [--state S[,S]] [--label k=v]... your launches (agents: the ones they started)
|
|
126
130
|
hub launches show <launchID> · hub launches stop <launchID> (stop: exit 8 when it already ended)
|
|
127
131
|
|
|
@@ -278,8 +282,17 @@ const whyOf = (flags: Parsed["flags"]) => {
|
|
|
278
282
|
return why !== undefined ? { why } : {}
|
|
279
283
|
}
|
|
280
284
|
|
|
281
|
-
/** --no-wake and --
|
|
282
|
-
const sendFlags = (flags: Parsed["flags"]) =>
|
|
285
|
+
/** --no-wake, --urgent, --not-after and --key as SDK send options. */
|
|
286
|
+
const sendFlags = (flags: Parsed["flags"]) => {
|
|
287
|
+
const notAfter = str(flags, "not-after")
|
|
288
|
+
const key = str(flags, "key")
|
|
289
|
+
return {
|
|
290
|
+
...(flags["no-wake"] === true ? { wake: false } : {}),
|
|
291
|
+
...(flags.urgent === true ? { urgent: true } : {}),
|
|
292
|
+
...(notAfter === undefined ? {} : { notAfter: notAfterArg(notAfter, "--not-after") }),
|
|
293
|
+
...(key !== undefined ? { idempotencyKey: key } : {}),
|
|
294
|
+
}
|
|
295
|
+
}
|
|
283
296
|
|
|
284
297
|
const nodeIo = (io: Io): NodeIo => ({
|
|
285
298
|
env: io.env,
|
|
@@ -399,7 +412,7 @@ const COMMANDS: Record<string, Command> = {
|
|
|
399
412
|
},
|
|
400
413
|
msg: {
|
|
401
414
|
args: ["<addr>", "<text>"],
|
|
402
|
-
flags: { all: "bool", "no-wake": "bool", urgent: "bool" },
|
|
415
|
+
flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string" },
|
|
403
416
|
async handle(context) {
|
|
404
417
|
const { flags, args } = context.parsed
|
|
405
418
|
const text = await textArg(args[1]!, context.io.stdin)
|
|
@@ -410,7 +423,7 @@ const COMMANDS: Record<string, Command> = {
|
|
|
410
423
|
},
|
|
411
424
|
ask: {
|
|
412
425
|
args: ["<addr>", "<text>"],
|
|
413
|
-
flags: { all: "bool", "no-wake": "bool", urgent: "bool", expires: "string", wait: "optional", ...DECISION_FLAGS },
|
|
426
|
+
flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string", expires: "string", wait: "optional", ...DECISION_FLAGS },
|
|
414
427
|
async handle(context) {
|
|
415
428
|
const { flags, args } = context.parsed
|
|
416
429
|
const decision = decisionOptions(flags, args[0]!)
|
package/packages/cli/src/http.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path"
|
|
|
5
5
|
import type { LaunchSpec, LaunchStep, LaunchView } from "../../sdk/src/index.ts"
|
|
6
6
|
import { CLEANUP_POLICIES, LAUNCH_END_WHEN, RUNTIME_KINDS, WORKSPACE_MODES } from "../../protocol/src/launch.ts"
|
|
7
7
|
import { str, UsageError, type FlagKind, type Parsed } from "./args"
|
|
8
|
+
import { notAfterArg } from "./messaging"
|
|
8
9
|
|
|
9
10
|
export const LAUNCH_FLAGS: Readonly<Record<string, FlagKind>> = {
|
|
10
11
|
key: "string",
|
|
@@ -28,6 +29,7 @@ export const LAUNCH_FLAGS: Readonly<Record<string, FlagKind>> = {
|
|
|
28
29
|
end: "string",
|
|
29
30
|
timeout: "string",
|
|
30
31
|
cleanup: "string",
|
|
32
|
+
"not-after": "string",
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** `a, b or c` */
|
|
@@ -93,10 +95,12 @@ export async function launchSpecArg(flags: Parsed["flags"], context: LaunchArgCo
|
|
|
93
95
|
const runtime = kind === undefined ? undefined : { kind, ...(profile !== undefined ? { profile } : {}) }
|
|
94
96
|
const placement = defined({ labels: nodeLabels, machine, mode: flags.prefer === true ? ("prefer" as const) : undefined })
|
|
95
97
|
const end = defined({ when, timeout: str(flags, "timeout"), cleanup })
|
|
98
|
+
const notAfterText = str(flags, "not-after")
|
|
99
|
+
const notAfter = notAfterText === undefined ? undefined : notAfterArg(notAfterText, "--not-after")
|
|
96
100
|
return {
|
|
97
101
|
...defined({ key: str(flags, "key"), name, roles, harness: str(flags, "harness"), agent: str(flags, "agent"), model: str(flags, "model") }),
|
|
98
102
|
workspace,
|
|
99
|
-
...defined({ runtime, placement, prompt, labels, end }),
|
|
103
|
+
...defined({ runtime, placement, prompt, labels, end, notAfter }),
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
package/packages/cli/src/mcp.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { intentLine, writeIntent } from "./intents"
|
|
|
18
18
|
import type { Credentials } from "./credentials"
|
|
19
19
|
import { logText, oneLine, sessionsText, showText, statusText, whoamiText, whoText } from "./format"
|
|
20
20
|
import { CliError, withHub } from "./http"
|
|
21
|
-
import { askOutcome, declinedText, deliveryText, durationArg, mutedText, repliedText, sentText } from "./messaging"
|
|
21
|
+
import { askOutcome, declinedText, deliveryText, durationArg, mutedText, notAfterArg, repliedText, sentText } from "./messaging"
|
|
22
22
|
import { DECISION_STATES, decisionsText, decisionText, openedText } from "./decisions"
|
|
23
23
|
import { CLEANUP_POLICIES, LAUNCH_END_WHEN, RUNTIME_KINDS, WORKSPACE_MODES } from "../../protocol/src/launch.ts"
|
|
24
24
|
import { launchedText, launchesText, launchText, stoppedLaunchText } from "./launches"
|
|
@@ -103,11 +103,27 @@ const optionalExpires = (args: Args): number | undefined => {
|
|
|
103
103
|
}
|
|
104
104
|
throw new ArgError(`"expires" must be seconds or a duration like 30s, 5m, 2h, 1d`)
|
|
105
105
|
}
|
|
106
|
-
/**
|
|
106
|
+
/** notAfter (H2): epoch milliseconds, or an ISO 8601 date-time converted to them. */
|
|
107
|
+
const optionalNotAfter = (args: Args): number | undefined => {
|
|
108
|
+
const value = args.notAfter
|
|
109
|
+
if (value === undefined || value === null) return undefined
|
|
110
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return value
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
try {
|
|
113
|
+
return notAfterArg(value, "\"notAfter\"")
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
if (cause instanceof UsageError) throw new ArgError(cause.message)
|
|
116
|
+
throw cause
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw new ArgError(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`)
|
|
120
|
+
}
|
|
121
|
+
/** The wake, urgent and notAfter arguments as SDK send options. */
|
|
107
122
|
const sendOptions = (args: Args) => {
|
|
108
123
|
const wake = optionalBool(args, "wake")
|
|
109
124
|
const urgent = optionalBool(args, "urgent")
|
|
110
|
-
|
|
125
|
+
const notAfter = optionalNotAfter(args)
|
|
126
|
+
return { ...(wake !== undefined ? { wake } : {}), ...(urgent !== undefined ? { urgent } : {}), ...(notAfter !== undefined ? { notAfter } : {}) }
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
/** The why argument as SDK act options. */
|
|
@@ -178,6 +194,7 @@ function launchSpec(args: Args): LaunchSpec {
|
|
|
178
194
|
prompt: optional(args, "prompt"),
|
|
179
195
|
labels: optionalLabels(args, "labels"),
|
|
180
196
|
end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) }),
|
|
197
|
+
notAfter: optionalNotAfter(args),
|
|
181
198
|
}),
|
|
182
199
|
}
|
|
183
200
|
}
|
|
@@ -220,6 +237,7 @@ const STRING = { type: "string" } as const
|
|
|
220
237
|
const SEND = {
|
|
221
238
|
wake: { type: "boolean", description: "false: admit the message without starting a turn." },
|
|
222
239
|
urgent: { type: "boolean", description: "Steer into the recipient's running turn." },
|
|
240
|
+
notAfter: { type: ["number", "string"], description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is sent (expired)." },
|
|
223
241
|
all: { type: "boolean", description: "Send to every session the address names (each holder of a role)." },
|
|
224
242
|
} as const
|
|
225
243
|
const TO = { type: "string", description: "The recipient: @name, @@role, @@role#team (a role of one of your teams), @name.project, ses_…, or @me (the user)." } as const
|
|
@@ -388,7 +406,7 @@ const webhookText = (w: { id: string; url: string; events: readonly string[]; la
|
|
|
388
406
|
const webhookTools: readonly Tool[] = [
|
|
389
407
|
{
|
|
390
408
|
name: "webhook_create",
|
|
391
|
-
description: "Create a durable signed webhook for canonical compact events. The signing secret is returned only here. Needs events:subscribe plus every selected family's scope.",
|
|
409
|
+
description: "Create a durable signed webhook for canonical compact events (e.g. launch.*, session.*, message.changed, team.intake). The signing secret is returned only here. Needs events:subscribe plus every selected family's scope; events [\"team.intake\"] alone needs only teams:submit (an app gets only its own submissions).",
|
|
392
410
|
properties: { url: STRING, events: { type: "array", items: STRING }, label: STRING }, required: ["url"], readOnly: false,
|
|
393
411
|
run: async (hub, args, signal) => {
|
|
394
412
|
const events = optionalStrings(args, "events")
|
|
@@ -809,13 +827,13 @@ const TOOLS: readonly Tool[] = [
|
|
|
809
827
|
{
|
|
810
828
|
name: "msg",
|
|
811
829
|
description: "Tell a session something (fire and forget). Sent as this server's credential, not as an OpenCode session.",
|
|
812
|
-
properties: { to: TO, text: { ...STRING, description: "The message." }, ...SEND },
|
|
830
|
+
properties: { to: TO, text: { ...STRING, description: "The message." }, ...SEND, idempotencyKey: IDEMPOTENCY },
|
|
813
831
|
required: ["to", "text"],
|
|
814
832
|
readOnly: false,
|
|
815
833
|
run: async (hub, args, signal) => {
|
|
816
834
|
const to = need(args, "to")
|
|
817
835
|
const text = need(args, "text")
|
|
818
|
-
const options = { ...sendOptions(args), signal }
|
|
836
|
+
const options = { ...sendOptions(args), ...idemOption(args), signal }
|
|
819
837
|
if (optionalBool(args, "all")) return sentAllText(await hub.msg(to, text, { ...options, all: true }))
|
|
820
838
|
return sentText(await hub.msg(to, text, options))
|
|
821
839
|
},
|
|
@@ -829,6 +847,7 @@ const TOOLS: readonly Tool[] = [
|
|
|
829
847
|
wait: { type: "number", minimum: 0, maximum: WAIT_POLL_MAX_S, description: `Seconds to wait for the answer (0–${WAIT_POLL_MAX_S}).` },
|
|
830
848
|
expires: { type: ["number", "string"], description: "When the ask expires: seconds, or 30s, 5m, 2h, 1d (1 minute to 7 days; default 24 h)." },
|
|
831
849
|
...SEND,
|
|
850
|
+
idempotencyKey: IDEMPOTENCY,
|
|
832
851
|
},
|
|
833
852
|
required: ["to", "text"],
|
|
834
853
|
readOnly: false,
|
|
@@ -837,7 +856,7 @@ const TOOLS: readonly Tool[] = [
|
|
|
837
856
|
const text = need(args, "text")
|
|
838
857
|
const wait = optionalWait(args)
|
|
839
858
|
const expires = optionalExpires(args)
|
|
840
|
-
const options = { ...sendOptions(args), ...(expires !== undefined ? { expires } : {}), signal }
|
|
859
|
+
const options = { ...sendOptions(args), ...(expires !== undefined ? { expires } : {}), ...idemOption(args), signal }
|
|
841
860
|
if (optionalBool(args, "all")) {
|
|
842
861
|
if (wait !== undefined) throw new ArgError(`"wait" cannot be combined with "all"`)
|
|
843
862
|
return sentAllText(await hub.ask(to, text, { ...options, all: true }))
|
|
@@ -999,7 +1018,7 @@ const TOOLS: readonly Tool[] = [
|
|
|
999
1018
|
{
|
|
1000
1019
|
name: "launch",
|
|
1001
1020
|
description:
|
|
1002
|
-
"Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
|
|
1021
|
+
"Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. With notAfter, a launch still waiting for a machine at that time is never started (it ends expired). Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
|
|
1003
1022
|
properties: {
|
|
1004
1023
|
repo: { ...STRING, description: "Absolute path of the repository on the node (or where a clone goes)." },
|
|
1005
1024
|
mode: { type: "string", enum: [...WORKSPACE_MODES], description: "worktree (default: <repo>/.worktrees/<branch>, needs branch), checkout, or clone (needs remote)." },
|
|
@@ -1022,6 +1041,7 @@ const TOOLS: readonly Tool[] = [
|
|
|
1022
1041
|
end: { type: "string", enum: [...LAUNCH_END_WHEN], description: "When it ends: idle (default; idle with nothing open for 30 s), asked (with a question open: needs_user), never." },
|
|
1023
1042
|
timeout: { ...STRING, description: "Longest run, e.g. 4h or 90m." },
|
|
1024
1043
|
cleanup: { type: "string", enum: [...CLEANUP_POLICIES], description: "keep (default) or remove the workspace when it ends." },
|
|
1044
|
+
notAfter: { type: ["number", "string"], description: "Do not start after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Still waiting for a machine then, the launch ends expired; once placed it runs (timeout bounds it). Already passed: nothing is launched (expired)." },
|
|
1025
1045
|
},
|
|
1026
1046
|
required: ["repo"],
|
|
1027
1047
|
readOnly: false,
|
|
@@ -57,6 +57,20 @@ export function durationArg(value: string, flag: string): number {
|
|
|
57
57
|
return Number(match[1]) * UNIT_S[match[2]!]!
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** An ISO 8601 date-time with at least hours and minutes; no zone means local time. */
|
|
61
|
+
const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?$/i
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A notAfter (H2) as epoch milliseconds, from epoch milliseconds (digits) or an ISO 8601 date-time like
|
|
65
|
+
* 2027-01-15T12:00:00Z; the hub checks the range (future, at most 30 days ahead). Throws UsageError(`what` …).
|
|
66
|
+
*/
|
|
67
|
+
export function notAfterArg(value: string, what: string): number {
|
|
68
|
+
const text = value.trim()
|
|
69
|
+
const ms = /^\d+$/.test(text) ? Number(text) : ISO_DATE_TIME.test(text) ? Date.parse(text) : Number.NaN
|
|
70
|
+
if (!Number.isSafeInteger(ms)) throw new UsageError(`${what} takes epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z (got "${value}")`)
|
|
71
|
+
return ms
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
const iso = (ms: number) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
61
75
|
|
|
62
76
|
/** What an ask came to, its text, and the exit code: answered 0, declined 1, open after a wait 1, expired 8. */
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// sessions submit under team policy and, in the team's entry role, report status.
|
|
6
6
|
import { sanitizeText } from "../../sdk/src/envelope.ts"
|
|
7
7
|
import { reportIntakeStatus, TEAM_INTAKE_STATUS_INPUTS, type TeamIntake, type TeamIntakeApi, type TeamIntakePriority, type TeamIntakeStatus, type TeamIntakeStatusInput } from "../../sdk/src/team-intake.ts"
|
|
8
|
+
import { UsageError } from "./args"
|
|
9
|
+
import { notAfterArg } from "./messaging"
|
|
8
10
|
import { ToolArgError } from "./tool-args"
|
|
9
11
|
|
|
10
12
|
type Args = Record<string, unknown>
|
|
@@ -60,6 +62,22 @@ function dueOf(args: Args): number | undefined {
|
|
|
60
62
|
return ms
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/** `notAfter` (H4) as epoch milliseconds or an ISO 8601 date-time; the hub checks the range. */
|
|
66
|
+
function notAfterOf(args: Args): number | undefined {
|
|
67
|
+
const value = args.notAfter
|
|
68
|
+
if (value === undefined || value === null || value === "") return undefined
|
|
69
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return value
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
try {
|
|
72
|
+
return notAfterArg(value, `"notAfter"`)
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
if (cause instanceof UsageError) throw new ToolArgError(cause.message)
|
|
75
|
+
throw cause
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new ToolArgError(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`)
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
const line = (i: TeamIntake) =>
|
|
64
82
|
`${i.id} ${i.status}${i.progress ? `/${i.progress}` : ""} ${i.priority} r${i.revision} ${sanitizeText(i.title).replace(/\s+/g, " ")}`
|
|
65
83
|
|
|
@@ -71,6 +89,7 @@ function intakeText(i: TeamIntake): string {
|
|
|
71
89
|
`status: ${i.status}${i.progress ? ` (${i.progress})` : ""} priority: ${i.priority} from: ${from}`,
|
|
72
90
|
...(i.assignmentID ? [`assignment: ${i.assignmentID} in workspace ${i.workspaceID ?? "?"}${i.sessionID ? `, delivered to ${i.sessionID}` : ""}`] : []),
|
|
73
91
|
...(i.due !== undefined ? [`due: ${new Date(i.due).toISOString()}`] : []),
|
|
92
|
+
...(i.notAfter !== undefined ? [`not after: ${new Date(i.notAfter).toISOString()}`] : []),
|
|
74
93
|
...(i.labels?.length ? [`labels: ${i.labels.join(", ")}`] : []),
|
|
75
94
|
...(i.criteria?.length ? ["done means:", ...i.criteria.map((c) => `- ${sanitizeText(c)}`)] : []),
|
|
76
95
|
...(i.ref ? [`ref: ${sanitizeText(i.ref)}`] : []),
|
|
@@ -83,7 +102,7 @@ export const teamIntakeTools: readonly TeamIntakeTool[] = [
|
|
|
83
102
|
{
|
|
84
103
|
name: "team_submit",
|
|
85
104
|
description:
|
|
86
|
-
"Give work to a team: its entry role (e.g. the project manager) receives the task with the team's context and owns it as an assignment in the team's workspace; while the role is vacant it waits queued. Pass `key` to make retries idempotent. App tokens need scope teams:submit; sessions submit under the team's communication policy.",
|
|
105
|
+
"Give work to a team: its entry role (e.g. the project manager) receives the task with the team's context and owns it as an assignment in the team's workspace; while the role is vacant it waits queued (with `notAfter`, only until then: still queued at that time, it is cancelled with reason not_after instead of delivered late). Pass `key` to make retries idempotent. App tokens need scope teams:submit; sessions submit under the team's communication policy.",
|
|
87
106
|
properties: {
|
|
88
107
|
team: TEAM,
|
|
89
108
|
title: { ...STRING, description: "One line, at most 120 characters." },
|
|
@@ -91,6 +110,10 @@ export const teamIntakeTools: readonly TeamIntakeTool[] = [
|
|
|
91
110
|
priority: { type: "string", enum: PRIORITIES, description: "Default normal." },
|
|
92
111
|
labels: { type: "array", items: STRING, description: "Slugs (lowercase letters, digits, dashes)." },
|
|
93
112
|
due: { ...STRING, description: "When it is due: an ISO date/time." },
|
|
113
|
+
notAfter: {
|
|
114
|
+
type: ["number", "string"],
|
|
115
|
+
description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is submitted (expired). Still queued then: cancelled (reason not_after).",
|
|
116
|
+
},
|
|
94
117
|
criteria: { type: "array", items: STRING, description: "What done means (at most 8 lines)." },
|
|
95
118
|
ref: { ...STRING, description: "An external reference, e.g. a Brain task ID." },
|
|
96
119
|
key: { ...STRING, description: "Idempotency-Key: the same key and body return the original submission." },
|
|
@@ -103,12 +126,13 @@ export const teamIntakeTools: readonly TeamIntakeTool[] = [
|
|
|
103
126
|
const labels = strings(args, "labels")
|
|
104
127
|
const criteria = strings(args, "criteria")
|
|
105
128
|
const due = dueOf(args)
|
|
129
|
+
const notAfter = notAfterOf(args)
|
|
106
130
|
const brief = optional(args, "brief")
|
|
107
131
|
const ref = optional(args, "ref")
|
|
108
132
|
const key = optional(args, "key")
|
|
109
133
|
const { intake, created } = await hub.teams.submit(
|
|
110
134
|
need(args, "team"),
|
|
111
|
-
{ title: need(args, "title"), ...(brief ? { brief } : {}), ...(priority ? { priority } : {}), ...(labels ? { labels } : {}), ...(due !== undefined ? { due } : {}), ...(criteria ? { criteria } : {}), ...(ref ? { ref } : {}) },
|
|
135
|
+
{ title: need(args, "title"), ...(brief ? { brief } : {}), ...(priority ? { priority } : {}), ...(labels ? { labels } : {}), ...(due !== undefined ? { due } : {}), ...(notAfter !== undefined ? { notAfter } : {}), ...(criteria ? { criteria } : {}), ...(ref ? { ref } : {}) },
|
|
112
136
|
{ signal, ...(key ? { idempotencyKey: key } : {}) },
|
|
113
137
|
)
|
|
114
138
|
return `${created ? "Submitted" : "Already submitted (same key)"}: ${intake.id} is ${intake.status}${intake.status === "queued" ? " (waiting for the entry role)" : ""}\n${intakeText(intake)}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-codex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huynle/hub-preset",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
|
|
6
6
|
"license": "MIT",
|