@fungi.computer/shiitake-extensions 0.1.1 → 0.3.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.
@@ -0,0 +1,73 @@
1
+ import type { Goal } from "./contract.js";
2
+ /** The `/goal` menu for a live or paused goal. */
3
+ export declare function goalMenuPrompt(goal: Goal, now: number): {
4
+ type: "choice";
5
+ message: string;
6
+ summary: string;
7
+ facts: {
8
+ label: string;
9
+ value: string;
10
+ }[];
11
+ details: {
12
+ label: string;
13
+ value: string;
14
+ }[];
15
+ options: ({
16
+ value: string;
17
+ label: string;
18
+ destructive?: never;
19
+ } | {
20
+ value: string;
21
+ label: string;
22
+ destructive: true;
23
+ })[];
24
+ } | {
25
+ body: string;
26
+ bodyLabel: string;
27
+ type: "choice";
28
+ message: string;
29
+ summary: string;
30
+ facts: {
31
+ label: string;
32
+ value: string;
33
+ }[];
34
+ details: {
35
+ label: string;
36
+ value: string;
37
+ }[];
38
+ options: ({
39
+ value: string;
40
+ label: string;
41
+ destructive?: never;
42
+ } | {
43
+ value: string;
44
+ label: string;
45
+ destructive: true;
46
+ })[];
47
+ };
48
+ /** The destructive step after choosing Clear: Keep is first and focused. */
49
+ export declare const clearConfirmation: {
50
+ type: "confirmation";
51
+ message: string;
52
+ summary: string;
53
+ confirmLabel: string;
54
+ cancelLabel: string;
55
+ destructive: true;
56
+ };
57
+ /** The destructive step after typing a replacement goal. */
58
+ export declare function replaceConfirmation(current: Goal, replacement: string): {
59
+ type: "confirmation";
60
+ message: string;
61
+ summary: string;
62
+ facts: {
63
+ label: string;
64
+ value: string;
65
+ }[];
66
+ details: {
67
+ label: string;
68
+ value: string;
69
+ }[];
70
+ confirmLabel: string;
71
+ cancelLabel: string;
72
+ destructive: true;
73
+ };
@@ -0,0 +1,98 @@
1
+ import { headline, titleLimit } from "../internal/headline.js";
2
+ const pad = (value) => String(value).padStart(2, "0");
3
+ /** A start time a person reads: 14:02 today, yesterday 14:02, or a full date. */
4
+ function startedAt(epochMillis, now) {
5
+ const at = new Date(epochMillis);
6
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}`;
7
+ const day = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
8
+ const days = Math.round((day(new Date(now)) - day(at)) / 86_400_000);
9
+ if (days === 0)
10
+ return `${clock} today`;
11
+ if (days === 1)
12
+ return `${clock} yesterday`;
13
+ return `${String(at.getFullYear())}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${clock}`;
14
+ }
15
+ /** What happened, by the typed cause and never by the goal's own words. */
16
+ const pausedSummary = {
17
+ person: "You paused this goal.",
18
+ blocked: "The Agent is stuck and needs you.",
19
+ stalled: "No progress for 3 turns, so Fungi paused it.",
20
+ cancelled: "You stopped the goal's turn.",
21
+ failed: "The last turn failed. Resume to try again.",
22
+ "no-terminal": "The goal's last turn ended without a final reply.",
23
+ };
24
+ const activeSummary = "Fungi keeps working until it is done or you pause it.";
25
+ /** The `/goal` menu for a live or paused goal. */
26
+ export function goalMenuPrompt(goal, now) {
27
+ const active = goal.state === "active";
28
+ const lead = active ? "Working on: " : "Goal paused: ";
29
+ const name = headline(goal.objective, titleLimit - lead.length);
30
+ const cause = goal.pauseCause;
31
+ const full = goal.objective.slice(0, 4_000);
32
+ const menu = {
33
+ type: "choice",
34
+ message: `${lead}${name}`,
35
+ summary: active ? activeSummary : pausedSummary[cause?.kind ?? "person"],
36
+ facts: [
37
+ { label: "Started", value: startedAt(goal.startedAt, now) },
38
+ ...(!active && cause?.kind === "blocked" && cause.detail != null
39
+ ? [{ label: "Why", value: `“${cause.detail.slice(0, 500)}”` }]
40
+ : []),
41
+ ],
42
+ details: [
43
+ { label: "Goal", value: full },
44
+ ...(cause === null
45
+ ? []
46
+ : [
47
+ {
48
+ label: "Paused",
49
+ value: cause.detail == null
50
+ ? cause.kind
51
+ : `${cause.kind} · ${cause.detail.slice(0, 2_000)}`,
52
+ },
53
+ ]),
54
+ { label: "Started", value: new Date(goal.startedAt).toLocaleString() },
55
+ ],
56
+ options: [
57
+ active
58
+ ? { value: "pause", label: "Pause goal" }
59
+ : { value: "resume", label: "Resume goal" },
60
+ { value: "replace", label: "Replace goal…" },
61
+ { value: "clear-ask", label: "Clear goal…", destructive: true },
62
+ ],
63
+ };
64
+ // A one-sentence goal is already the title; only longer text gets a body.
65
+ return full.replace(/\s+/gu, " ").trim() === name
66
+ ? menu
67
+ : { ...menu, body: full, bodyLabel: "Goal" };
68
+ }
69
+ /** The destructive step after choosing Clear: Keep is first and focused. */
70
+ export const clearConfirmation = {
71
+ type: "confirmation",
72
+ message: "Clear this goal?",
73
+ summary: "It moves to history and can't be resumed.",
74
+ confirmLabel: "Clear goal",
75
+ cancelLabel: "Keep goal",
76
+ destructive: true,
77
+ };
78
+ /** The destructive step after typing a replacement goal. */
79
+ export function replaceConfirmation(current, replacement) {
80
+ const old = current.objective.slice(0, 4_000);
81
+ const next = replacement.slice(0, 4_000);
82
+ return {
83
+ type: "confirmation",
84
+ message: `Replace the ${current.state === "active" ? "live" : "paused"} goal?`,
85
+ summary: "The current goal moves to history and can't be resumed.",
86
+ facts: [
87
+ { label: "Current", value: headline(old, 120) },
88
+ { label: "New", value: headline(next, 120) },
89
+ ],
90
+ details: [
91
+ { label: "Current", value: old },
92
+ { label: "New", value: next },
93
+ ],
94
+ confirmLabel: "Replace and start",
95
+ cancelLabel: "Keep current goal",
96
+ destructive: true,
97
+ };
98
+ }
@@ -14,8 +14,13 @@ export declare const homeGrant: ExtensionGrantV0;
14
14
  /** Agent-scope choice with per-run tools and prompt over one captured table. */
15
15
  export declare function homeExtension(options: {
16
16
  computers: {
17
+ /** Read the table inside the synchronous Home choice transaction. */
17
18
  read: (context: Readonly<{
18
19
  sessionId: string;
19
20
  }>) => readonly HomeComputer[];
21
+ /** Optional host preparation before run-scoped Files capture. */
22
+ prepare?: (context: Readonly<{
23
+ sessionId: string;
24
+ }>) => Promise<void>;
20
25
  };
21
26
  }): RosteredExtensionV0;
package/dist/home/host.js CHANGED
@@ -51,7 +51,8 @@ export function homeExtension(options) {
51
51
  schema: Schema.NullOr(Home),
52
52
  read: (ctx) => Option.some(Option.getOrNull(ctx.state.choice.peek("default"))),
53
53
  },
54
- files: (ctx, run) => {
54
+ files: async (ctx, run) => {
55
+ await options.computers.prepare?.(run);
55
56
  const computers = [...options.computers.read(run)];
56
57
  const choice = ctx.state.update((state) => {
57
58
  const recorded = Option.getOrNull(state.choice.get("default"));
@@ -99,14 +100,20 @@ export function homeExtension(options) {
99
100
  type: "info",
100
101
  message: "No Computers attached. Attach one before choosing Home.",
101
102
  });
102
- return ask({
103
+ const recorded = Option.getOrNull(yield* ctx.state.choice.get("default"));
104
+ const home = computers.find(({ attachmentId }) => attachmentId === recorded?.attachmentId);
105
+ const prompt = {
103
106
  type: "choice",
104
- message: "Choose this Agent's Home Computer",
107
+ message: "Choose a Home Computer",
108
+ summary: "The Agent reads and edits files here by default.",
105
109
  options: computers.map(({ name }) => ({
106
110
  value: name,
107
111
  label: name,
108
112
  })),
109
- }, {}, "name");
113
+ };
114
+ return ask(home === undefined
115
+ ? prompt
116
+ : { ...prompt, current: home.name }, {}, "name");
110
117
  }
111
118
  const selected = computers.find(({ name }) => name === input.name);
112
119
  if (selected === undefined)
@@ -0,0 +1,7 @@
1
+ /** Longest title a modal carries. */
2
+ export declare const titleLimit = 60;
3
+ /**
4
+ * A short name for long user text: its first line's first sentence, whitespace collapsed
5
+ * and cut at a word to `limit` characters with an ellipsis.
6
+ */
7
+ export declare function headline(objective: string, limit: number): string;
@@ -0,0 +1,18 @@
1
+ /** Longest title a modal carries. */
2
+ export const titleLimit = 60;
3
+ /**
4
+ * A short name for long user text: its first line's first sentence, whitespace collapsed
5
+ * and cut at a word to `limit` characters with an ellipsis.
6
+ */
7
+ export function headline(objective, limit) {
8
+ const line = objective
9
+ .split("\n")
10
+ .map((part) => part.replace(/\s+/gu, " ").trim())
11
+ .find((part) => part !== "") ?? "";
12
+ const sentence = (/^.*?[.!?](?=\s|$)/u.exec(line)?.[0] ?? line).replace(/[.!?]+$/u, "");
13
+ if (sentence.length <= limit)
14
+ return sentence;
15
+ const cut = sentence.slice(0, Math.max(0, limit - 1));
16
+ const space = cut.lastIndexOf(" ");
17
+ return `${(space > limit / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
18
+ }
@@ -74,7 +74,7 @@ export default defineClientEntry(loopContract, {
74
74
  cancel: {
75
75
  id: "loop",
76
76
  label: "stop loop",
77
- input: { action: "stop" },
77
+ input: { action: "stop-ask" },
78
78
  },
79
79
  }
80
80
  : null,
@@ -54,7 +54,9 @@ export declare const LoopCommandInput: Schema.Union<readonly [Schema.Struct<{
54
54
  readonly requestId: Schema.String;
55
55
  }>, Schema.Struct<{
56
56
  /** Select an existing-loop control or open new-loop entry. */
57
- readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "new"]>;
57
+ readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "stop-ask", "new"]>;
58
+ /** The human's answer to "stop this loop?"; absent for a typed stop. */
59
+ readonly confirmed: Schema.optional<Schema.Boolean>;
58
60
  }>, Schema.Struct<{}>]>;
59
61
  /** Loop's contract: Session-scope controls and one projection slot. */
60
62
  export declare const LoopProjection: Schema.Struct<{
@@ -111,7 +113,9 @@ export declare const loopContract: {
111
113
  readonly requestId: Schema.String;
112
114
  }>, Schema.Struct<{
113
115
  /** Select an existing-loop control or open new-loop entry. */
114
- readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "new"]>;
116
+ readonly action: Schema.Literals<readonly ["status", "pause", "resume", "stop", "stop-ask", "new"]>;
117
+ /** The human's answer to "stop this loop?"; absent for a typed stop. */
118
+ readonly confirmed: Schema.optional<Schema.Boolean>;
115
119
  }>, Schema.Struct<{}>]>;
116
120
  /** Return the server-owned conversation step. */
117
121
  readonly output: Schema.declare<import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1, import("@fungi.computer/whistle/catalog").WhistleCatalogStepV1>;
@@ -75,7 +75,16 @@ export const LoopCommandInput = Schema.Union([
75
75
  }),
76
76
  Schema.Struct({
77
77
  /** Select an existing-loop control or open new-loop entry. */
78
- action: Schema.Literals(["status", "pause", "resume", "stop", "new"]),
78
+ action: Schema.Literals([
79
+ "status",
80
+ "pause",
81
+ "resume",
82
+ "stop",
83
+ "stop-ask",
84
+ "new",
85
+ ]),
86
+ /** The human's answer to "stop this loop?"; absent for a typed stop. */
87
+ confirmed: Schema.optional(Schema.Boolean),
79
88
  }),
80
89
  Schema.Struct({}),
81
90
  ]);
package/dist/loop/host.js CHANGED
@@ -8,9 +8,10 @@ import * as Exit from "effect/Exit";
8
8
  import * as Option from "effect/Option";
9
9
  import * as Schema from "effect/Schema";
10
10
  import { z } from "zod";
11
- import { ask, catalog, done, freshRequestId } from "../internal/catalog.js";
11
+ import { ask, catalog, declined, done, freshRequestId, } from "../internal/catalog.js";
12
12
  import { nextOccurrence } from "../internal/cron.js";
13
13
  import { Loop, LoopCommandInput, loopContract } from "./contract.js";
14
+ import { loopMenuPrompt, stopConfirmation } from "./prompts.js";
14
15
  /** The rules the model follows; the describe page and a live loop's prompt share them. */
15
16
  const rules = "You may start loops yourself; one live loop per Session. Failed loop ticks retry with backoff; loop.get shows consecutiveFailures, lastError, and nextDueAt. schedule_next is allowed only in the active self-paced loop's own run, clamped to 60–3600 seconds. Omitting it ends that loop. stop works in any Session run. Human pause/resume/stop and Esc remain user controls.";
16
17
  /** Hashed prose and policy: every constant loop reads comes from here. */
@@ -521,7 +522,8 @@ function loopLine(loop) {
521
522
  }
522
523
  const loopPrompt = {
523
524
  type: "text",
524
- message: "What should the loop repeat? Start with 5m or 1h for a fixed interval.",
525
+ message: "What should the loop repeat?",
526
+ summary: "Start with 5m or 1h for a fixed interval.",
525
527
  placeholder: "Check the build and fix what broke",
526
528
  };
527
529
  /** The human `/loop` conversation, decided from the committed loop. */
@@ -533,16 +535,7 @@ function humanLoop(ctx, sessionId, input) {
533
535
  if (!("action" in input)) {
534
536
  if (current === undefined || !live)
535
537
  return ask(loopPrompt, { action: "start", requestId: yield* freshRequestId }, "text");
536
- return ask({
537
- type: "choice",
538
- message: loopLine(current),
539
- options: [
540
- current.state === "active"
541
- ? { value: "pause", label: "Pause loop" }
542
- : { value: "resume", label: "Resume loop" },
543
- { value: "stop", label: "Stop loop" },
544
- ],
545
- }, {}, "action");
538
+ return ask(loopMenuPrompt(current), {}, "action");
546
539
  }
547
540
  switch (input.action) {
548
541
  case "status":
@@ -564,7 +557,12 @@ function humanLoop(ctx, sessionId, input) {
564
557
  type: "success",
565
558
  message: loopLine(yield* resumeLoop(ctx, sessionId)),
566
559
  });
560
+ case "stop-ask":
561
+ // The menu's Stop: Keep is the focused answer to this destructive step.
562
+ return ask(stopConfirmation, { action: "stop" }, "confirmed");
567
563
  case "stop":
564
+ if (input.confirmed === false)
565
+ return declined({ type: "info", message: "Kept the loop." });
568
566
  return done({
569
567
  type: "success",
570
568
  message: loopLine(yield* stopLoop(ctx, sessionId)),
@@ -0,0 +1,56 @@
1
+ import type { Loop } from "./contract.js";
2
+ /** The `/loop` menu for a live or paused loop. */
3
+ export declare function loopMenuPrompt(loop: Loop): {
4
+ type: "choice";
5
+ message: string;
6
+ summary: string;
7
+ facts: {
8
+ label: string;
9
+ value: string;
10
+ }[];
11
+ details: {
12
+ label: string;
13
+ value: string;
14
+ }[];
15
+ options: ({
16
+ value: string;
17
+ label: string;
18
+ destructive?: never;
19
+ } | {
20
+ value: string;
21
+ label: string;
22
+ destructive: true;
23
+ })[];
24
+ } | {
25
+ body: string;
26
+ bodyLabel: string;
27
+ type: "choice";
28
+ message: string;
29
+ summary: string;
30
+ facts: {
31
+ label: string;
32
+ value: string;
33
+ }[];
34
+ details: {
35
+ label: string;
36
+ value: string;
37
+ }[];
38
+ options: ({
39
+ value: string;
40
+ label: string;
41
+ destructive?: never;
42
+ } | {
43
+ value: string;
44
+ label: string;
45
+ destructive: true;
46
+ })[];
47
+ };
48
+ /** The destructive step after choosing Stop: Keep is first and focused. */
49
+ export declare const stopConfirmation: {
50
+ type: "confirmation";
51
+ message: string;
52
+ summary: string;
53
+ confirmLabel: string;
54
+ cancelLabel: string;
55
+ destructive: true;
56
+ };
@@ -0,0 +1,54 @@
1
+ import { headline, titleLimit } from "../internal/headline.js";
2
+ /** The `/loop` menu for a live or paused loop. */
3
+ export function loopMenuPrompt(loop) {
4
+ const active = loop.state === "active";
5
+ const lead = active ? "Loop running: " : "Loop paused: ";
6
+ const name = headline(loop.prompt, titleLimit - lead.length);
7
+ const full = loop.prompt.slice(0, 4_000);
8
+ const menu = {
9
+ type: "choice",
10
+ message: `${lead}${name}`,
11
+ summary: loop.mode === "self_paced"
12
+ ? "The Agent decides when to run it again."
13
+ : "It runs again on a fixed schedule.",
14
+ facts: [
15
+ {
16
+ label: "Mode",
17
+ value: loop.mode === "self_paced" ? "Self-paced" : "Fixed interval",
18
+ },
19
+ {
20
+ label: "Started",
21
+ value: loop.origin === "human" ? "By you" : "By the Agent",
22
+ },
23
+ ],
24
+ details: [
25
+ { label: "Prompt", value: full },
26
+ ...(loop.lastError === null
27
+ ? []
28
+ : [{ label: "Last error", value: loop.lastError.slice(0, 2_000) }]),
29
+ {
30
+ label: "Failures",
31
+ value: String(loop.consecutiveFailures),
32
+ },
33
+ ],
34
+ options: [
35
+ active
36
+ ? { value: "pause", label: "Pause loop" }
37
+ : { value: "resume", label: "Resume loop" },
38
+ { value: "stop-ask", label: "Stop loop…", destructive: true },
39
+ ],
40
+ };
41
+ // A short prompt is already the title; only longer text gets a body.
42
+ return full.replace(/\s+/gu, " ").trim() === name
43
+ ? menu
44
+ : { ...menu, body: full, bodyLabel: "Prompt" };
45
+ }
46
+ /** The destructive step after choosing Stop: Keep is first and focused. */
47
+ export const stopConfirmation = {
48
+ type: "confirmation",
49
+ message: "Stop this loop?",
50
+ summary: "It won't run again. You can start a new one.",
51
+ confirmLabel: "Stop loop",
52
+ cancelLabel: "Keep loop",
53
+ destructive: true,
54
+ };
@@ -8,6 +8,7 @@ import * as Schema from "effect/Schema";
8
8
  import { createHash } from "node:crypto";
9
9
  import { z } from "zod";
10
10
  import Papa from "papaparse";
11
+ import { titleLimit } from "../internal/headline.js";
11
12
  import { ask, catalog, done, freshRequestId } from "../internal/catalog.js";
12
13
  import { questionContract, OpenAsk, Questions, Answers, HumanResult, DeclineInput, ClosedStatus, AnswerFlowInput, } from "./contract.js";
13
14
  const config = {
@@ -695,15 +696,49 @@ function singleSelection(question, selection) {
695
696
  }))
696
697
  : Effect.succeed([option.label]);
697
698
  }
699
+ /**
700
+ * The Agent's own words are the title only when they fit one; a longer
701
+ * question gets a plain title and sits in the body, whole in the details.
702
+ */
703
+ function questionHeading(question) {
704
+ const text = question.question.replace(/\s+/gu, " ").trim();
705
+ if (text.length <= titleLimit)
706
+ return { message: text };
707
+ return {
708
+ message: "The Agent has a question",
709
+ body: question.question.slice(0, 4_000),
710
+ bodyLabel: "Question",
711
+ details: [{ label: "Question", value: question.question.slice(0, 4_000) }],
712
+ };
713
+ }
698
714
  function promptQuestion(nextQuestion, next) {
715
+ const heading = questionHeading(nextQuestion);
699
716
  if (nextQuestion.multiple === true)
700
717
  return ask({
701
718
  type: "text",
702
- message: `${nextQuestion.question}\n${nextQuestion.options.map((option) => `${Papa.unparse([[option.label]])}: ${option.description}`).join("\n")}\nEnter selected labels as a comma list (quote labels containing commas), or leave blank for free text.`,
719
+ ...heading,
720
+ summary: `Separate your picks with commas, or leave blank to write your own.${nextQuestion.options.some((option) => option.label.includes(","))
721
+ ? " Put quotes around a pick that contains a comma."
722
+ : ""}`,
723
+ facts: [
724
+ {
725
+ label: "Options",
726
+ value: nextQuestion.options
727
+ .map((option) => option.label)
728
+ .join(" · "),
729
+ },
730
+ ],
731
+ details: [
732
+ ...("details" in heading ? heading.details : []),
733
+ ...nextQuestion.options.map((option) => ({
734
+ label: option.label.slice(0, 200),
735
+ value: option.description,
736
+ })),
737
+ ].slice(0, 24),
703
738
  }, { stage: "multiple", ...next }, "selection");
704
739
  return ask({
705
740
  type: "choice",
706
- message: nextQuestion.question,
741
+ ...heading,
707
742
  options: [
708
743
  ...nextQuestion.options.map((option) => ({
709
744
  value: `option:${option.label}`,
@@ -1,14 +1,16 @@
1
1
  import { type AgentControlDirectory } from "@fungi.computer/shiitake/agent-control";
2
2
  import type { CapabilitySource } from "@fungi.computer/mycelium";
3
3
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
4
- import { type SubagentSource } from "../subagents/host.js";
4
+ import type { SubagentSource } from "../subagents/host.js";
5
5
  /** Host-pack creation admission; attachments/model selection retain their own owners. */
6
6
  export declare function assertOrdinaryAgentCreation(input: Parameters<NonNullable<AgentControlDirectory["create"]>>[0]): void;
7
7
  /**
8
8
  * This host's own record of the Agents each conversation created or sent
9
9
  * accepted work to, written as its Agent-control directory accepts them
10
- * (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). It feeds
11
- * the footer only; it owns no relationship and grants nothing.
10
+ * (Wiki: agent-runtime-tools-and-clients, read-only sub-agent view). Accepted
11
+ * child command receipts are separately reconciled against their owner's
12
+ * settlement records for the parent's goal guard. Neither record grants
13
+ * authority over a child.
12
14
  */
13
15
  export declare function createConversationAgents(storage: WatchdogSqliteOwner): {
14
16
  /** Bind acceptance recording to this conversation's existing directory. */
@@ -16,16 +18,10 @@ export declare function createConversationAgents(storage: WatchdogSqliteOwner):
16
18
  /** Agent-control tools bound to the conversation that acquires them. */
17
19
  control: (directoryFor: (parentSessionId: string | undefined) => AgentControlDirectory) => CapabilitySource;
18
20
  /** The footer rows for one conversation; archived and unknown Agents leave it. */
19
- source: (directory: Pick<AgentControlDirectory, "list" | "open">) => SubagentSource;
21
+ source: (directory: Pick<AgentControlDirectory, "list"> & Pick<SubagentSource, "open">) => SubagentSource;
20
22
  };
21
23
  /** Both hosts compose the ordinary roster and its read-only created-Agent footer. */
22
- export declare function oysterAgentExtensions(source: SubagentSource): {
23
- /** Ordinary extensions plus the read-only created-Agent footer. */
24
- roster: import("@fungi.computer/shiitake").RosteredExtensionV0[];
25
- /** Host grants corresponding to the composed extension roster. */
26
- grants: Readonly<{
27
- id: import("@fungi.computer/shiitake/extensions").ExtensionId;
28
- capabilities: readonly import("@fungi.computer/shiitake").ExtensionCapabilityV0[];
29
- grantKey?: string;
30
- }>[];
31
- };
24
+ export declare function oysterAgentExtensions(source: SubagentSource): Readonly<{
25
+ roster: readonly import("@fungi.computer/shiitake").RosteredExtensionV0[];
26
+ grants: readonly import("@fungi.computer/shiitake").ExtensionGrantV0[];
27
+ }>;