opencode-courier 0.13.8 → 0.14.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/README.md +7 -9
- package/dist/index.d.ts +0 -8
- package/dist/index.js +6 -53
- package/dist/kernel/protocol.d.ts +0 -14
- package/dist/kernel/protocol.js +1 -15
- package/dist/kernel/tool.d.ts +0 -2
- package/dist/kernel/tool.js +0 -1
- package/dist/memory/domain.d.ts +6 -85
- package/dist/memory/domain.js +7 -63
- package/dist/memory/events.d.ts +0 -11
- package/dist/memory/events.js +1 -12
- package/dist/memory/forget.d.ts +0 -5
- package/dist/memory/forget.js +0 -4
- package/dist/memory/index.js +0 -2
- package/dist/memory/lessons.d.ts +0 -8
- package/dist/memory/lessons.js +0 -9
- package/dist/memory/note.d.ts +0 -9
- package/dist/memory/note.js +0 -5
- package/dist/memory/pin.d.ts +0 -5
- package/dist/memory/pin.js +0 -4
- package/dist/memory/ports.d.ts +2 -22
- package/dist/memory/recall.d.ts +0 -5
- package/dist/memory/recall.js +0 -12
- package/dist/memory/record.d.ts +0 -4
- package/dist/memory/record.js +8 -31
- package/dist/memory/rpc-handlers.d.ts +3 -6
- package/dist/memory/rpc-handlers.js +0 -6
- package/dist/memory/rpc.d.ts +6 -19
- package/dist/memory/rpc.js +1 -16
- package/dist/memory/store.d.ts +0 -5
- package/dist/memory/store.js +1 -16
- package/dist/memory/sweep.d.ts +0 -4
- package/dist/memory/sweep.js +0 -4
- package/dist/memory/text.d.ts +0 -25
- package/dist/memory/text.js +6 -36
- package/dist/memory/tools.d.ts +0 -2
- package/dist/memory/tools.js +0 -2
- package/dist/memory/tui.d.ts +1 -35
- package/dist/person/events.d.ts +0 -16
- package/dist/person/events.js +3 -30
- package/dist/person/form.d.ts +0 -9
- package/dist/person/form.js +0 -10
- package/dist/person/index.d.ts +0 -2
- package/dist/person/permission.d.ts +0 -34
- package/dist/person/permission.js +2 -28
- package/dist/person/ports.d.ts +0 -16
- package/dist/person/ports.js +0 -1
- package/dist/person/question/answer.d.ts +0 -28
- package/dist/person/question/answer.js +0 -49
- package/dist/person/question/index.js +0 -2
- package/dist/person/question/lifecycle.d.ts +0 -26
- package/dist/person/question/lifecycle.js +1 -32
- package/dist/person/question/pure.d.ts +0 -11
- package/dist/person/question/pure.js +0 -11
- package/dist/person/question/relay.d.ts +0 -1
- package/dist/person/question/relay.js +5 -67
- package/dist/person/question/runtime.d.ts +0 -14
- package/dist/person/question/runtime.js +1 -26
- package/dist/person/question/shared.d.ts +0 -17
- package/dist/person/question/shared.js +0 -20
- package/dist/person/question/types.d.ts +0 -44
- package/dist/person/rpc-handlers.d.ts +0 -7
- package/dist/person/rpc-handlers.js +0 -6
- package/dist/person/rpc.d.ts +0 -20
- package/dist/person/rpc.js +0 -16
- package/dist/person/text.d.ts +0 -45
- package/dist/person/text.js +2 -54
- package/dist/person/tools.d.ts +0 -3
- package/dist/person/tools.js +1 -3
- package/dist/person/tui.d.ts +0 -21
- package/dist/platform/bounded.d.ts +0 -8
- package/dist/platform/bounded.js +0 -8
- package/dist/platform/events.d.ts +0 -18
- package/dist/platform/events.js +3 -22
- package/dist/platform/hub.d.ts +0 -113
- package/dist/platform/hub.js +1 -76
- package/dist/platform/json.d.ts +0 -3
- package/dist/platform/json.js +1 -3
- package/dist/platform/storage.d.ts +0 -7
- package/dist/platform/storage.js +0 -6
- package/dist/platform/version.d.ts +0 -9
- package/dist/platform/version.js +0 -8
- package/dist/rpc.js +0 -1
- package/dist/scheduling/index.d.ts +0 -1
- package/dist/scheduling/index.js +0 -1
- package/dist/scheduling/later.d.ts +0 -11
- package/dist/scheduling/later.js +0 -14
- package/dist/scheduling/text.d.ts +0 -1
- package/dist/scheduling/text.js +0 -1
- package/dist/scheduling/tools.d.ts +0 -1
- package/dist/scheduling/tools.js +1 -5
- package/dist/skill.d.ts +1 -9
- package/dist/skill.js +4 -12
- package/dist/tree/cleanup.d.ts +0 -20
- package/dist/tree/cleanup.js +0 -26
- package/dist/tree/context.d.ts +0 -5
- package/dist/tree/context.js +0 -8
- package/dist/tree/domain.d.ts +0 -64
- package/dist/tree/domain.js +7 -45
- package/dist/tree/events.d.ts +0 -36
- package/dist/tree/events.js +4 -49
- package/dist/tree/forget.d.ts +0 -15
- package/dist/tree/forget.js +0 -18
- package/dist/tree/group.d.ts +0 -22
- package/dist/tree/group.js +1 -37
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.js +1 -3
- package/dist/tree/limits.d.ts +0 -4
- package/dist/tree/limits.js +1 -12
- package/dist/tree/mascot.d.ts +0 -25
- package/dist/tree/mascot.js +1 -32
- package/dist/tree/model.d.ts +0 -5
- package/dist/tree/model.js +2 -15
- package/dist/tree/nudge.d.ts +0 -11
- package/dist/tree/nudge.js +3 -16
- package/dist/tree/panel.d.ts +0 -8
- package/dist/tree/panel.js +3 -16
- package/dist/tree/ports.d.ts +2 -138
- package/dist/tree/report.d.ts +0 -10
- package/dist/tree/report.js +0 -11
- package/dist/tree/roster.d.ts +0 -21
- package/dist/tree/roster.js +0 -21
- package/dist/tree/rpc-handlers.d.ts +6 -1
- package/dist/tree/rpc-handlers.js +0 -5
- package/dist/tree/rpc.d.ts +9 -109
- package/dist/tree/rpc.js +2 -38
- package/dist/tree/running.d.ts +0 -6
- package/dist/tree/running.js +2 -10
- package/dist/tree/send.d.ts +0 -4
- package/dist/tree/send.js +4 -31
- package/dist/tree/spawn.d.ts +0 -7
- package/dist/tree/spawn.js +6 -47
- package/dist/tree/standing.d.ts +0 -8
- package/dist/tree/standing.js +1 -11
- package/dist/tree/status.d.ts +0 -8
- package/dist/tree/status.js +1 -9
- package/dist/tree/stop.d.ts +2 -9
- package/dist/tree/stop.js +58 -25
- package/dist/tree/store.d.ts +0 -8
- package/dist/tree/store.js +4 -38
- package/dist/tree/subtree.d.ts +8 -26
- package/dist/tree/subtree.js +37 -35
- package/dist/tree/summary.d.ts +0 -4
- package/dist/tree/summary.js +0 -8
- package/dist/tree/text.d.ts +1 -44
- package/dist/tree/text.js +5 -65
- package/dist/tree/tools.d.ts +0 -6
- package/dist/tree/tools.js +4 -8
- package/dist/tree/tui.d.ts +0 -81
- package/dist/tree/wake.d.ts +0 -30
- package/dist/tree/wake.js +5 -39
- package/dist/tui.js +22 -8
- package/package.json +8 -9
- package/dist/webhook/index.d.ts +0 -5
- package/dist/webhook/index.js +0 -3
- package/dist/webhook/receiver.d.ts +0 -80
- package/dist/webhook/receiver.js +0 -339
- package/dist/webhook/subscriptions.d.ts +0 -29
- package/dist/webhook/subscriptions.js +0 -52
- package/dist/webhook/text.d.ts +0 -23
- package/dist/webhook/text.js +0 -82
- package/dist/webhook/tools.d.ts +0 -19
- package/dist/webhook/tools.js +0 -46
package/dist/memory/text.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
/** The memory's model-facing texts: what courier_recall and courier_note say. */
|
|
2
1
|
import { clipText, clipWords, defuseBlock } from "../kernel/protocol.js";
|
|
3
2
|
import { LESSON_MAX, oneLine, PART_STATUSES, splitProse } from "./domain.js";
|
|
4
|
-
/** The tool's name, which the skill and the role part name too. */
|
|
5
3
|
export const RECALL_TOOL = "courier_recall";
|
|
6
|
-
/** The lesson tool's name, which the skill's last step names. */
|
|
7
4
|
export const NOTE_TOOL = "courier_note";
|
|
8
5
|
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
9
6
|
const DAY_MS = 86_400_000;
|
|
10
|
-
/** A duration in milliseconds as a model reads it: seconds under half a minute, else whole minutes, else hours and minutes. */
|
|
11
7
|
export function duration(ms) {
|
|
12
8
|
const minutes = Math.round(ms / 60_000);
|
|
13
9
|
if (minutes < 1)
|
|
@@ -16,39 +12,33 @@ export function duration(ms) {
|
|
|
16
12
|
return `${minutes} min`;
|
|
17
13
|
return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
|
|
18
14
|
}
|
|
19
|
-
/** How long ago a record last moved: a duration under a day, else whole days, which a record kept for months is read in. */
|
|
20
15
|
export function age(ms) {
|
|
21
16
|
const days = Math.floor(ms / DAY_MS);
|
|
22
17
|
return days < 1 ? duration(ms) : count(days, "day");
|
|
23
18
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return "no parts";
|
|
19
|
+
function partsLine(all) {
|
|
20
|
+
const parts = all.filter((part) => !part.subagent);
|
|
21
|
+
const subagents = all.length - parts.length;
|
|
28
22
|
const statuses = PART_STATUSES.flatMap((status) => {
|
|
29
23
|
const n = parts.filter((part) => part.status === status).length;
|
|
30
24
|
return n ? [`${n} ${status}`] : [];
|
|
31
25
|
}).join(", ");
|
|
32
26
|
const isolated = parts.filter((part) => part.isolated).length;
|
|
33
27
|
const refused = parts.reduce((n, part) => n + (part.refused ?? 0), 0);
|
|
34
|
-
const deepest = Math.max(...
|
|
28
|
+
const deepest = Math.max(0, ...all.map((part) => part.depth));
|
|
35
29
|
return [
|
|
36
|
-
`${count(parts.length, "part")}: ${statuses}
|
|
30
|
+
parts.length ? `${count(parts.length, "part")}: ${statuses}` : "no parts",
|
|
37
31
|
...(deepest > 1 ? [`${deepest} levels deep`] : []),
|
|
38
32
|
...(isolated ? [`${isolated} isolated`] : []),
|
|
39
33
|
...(refused ? [`${count(refused, "spawn")} refused`] : []),
|
|
34
|
+
...(subagents ? [count(subagents, "subagent")] : []),
|
|
40
35
|
].join(", ");
|
|
41
36
|
}
|
|
42
37
|
const day = (at) => new Date(at).toISOString().slice(0, 10);
|
|
43
|
-
/** A lesson as the listing and the note's result show it: dated, and from which depth when a session below the top wrote it. */
|
|
44
38
|
export function lessonLine(lesson) {
|
|
45
39
|
const from = lesson.depth === undefined ? "" : `, from depth ${lesson.depth}`;
|
|
46
40
|
return `lesson (${day(lesson.at)}${from}): "${clipText(oneLine(lesson.text), LESSON_MAX)}"`;
|
|
47
41
|
}
|
|
48
|
-
/**
|
|
49
|
-
* One record as the listing shows it: its top, when it started, its title, its split line and its latest revision as prose,
|
|
50
|
-
* its parts, how long it ran, how long ago it last moved, what it cost, and whether the person pinned it.
|
|
51
|
-
*/
|
|
52
42
|
export function recordLine(record, top, now) {
|
|
53
43
|
const cost = record.parts.reduce((sum, part) => sum + (part.cost ?? 0), 0);
|
|
54
44
|
const costed = record.parts.some((part) => part.cost !== undefined);
|
|
@@ -67,14 +57,8 @@ export function recordLine(record, top, now) {
|
|
|
67
57
|
...(record.lesson ? [lessonLine(record.lesson)] : []),
|
|
68
58
|
].join(" · ");
|
|
69
59
|
}
|
|
70
|
-
/** What the listing says of pinned records, when it holds one. */
|
|
71
60
|
const PINNED = "A record marked \"pinned\" is one the person chose to keep and listed first: weigh it before the rest.";
|
|
72
|
-
/** What courier_recall says when the memory option is off. */
|
|
73
61
|
export const RECALL_OFF = "Memory is off: the plugin's memory option is false, so no record of a session tree is kept or listed in this project. Decide from the task alone.";
|
|
74
|
-
/**
|
|
75
|
-
* The listing: every record of the project, the pinned first and then newest first, the caller's own tree marked, each
|
|
76
|
-
* aged against `now`.
|
|
77
|
-
*/
|
|
78
62
|
export function recallListText(records, top, now) {
|
|
79
63
|
const others = records.filter((record) => record.sessionID !== top);
|
|
80
64
|
const pinned = records.some((record) => record.pinned);
|
|
@@ -87,14 +71,11 @@ export function recallListText(records, top, now) {
|
|
|
87
71
|
: "No earlier tree is recorded in this project: a tree's record is written by a courier_recall call and as each turn in it ends, and kept 90 days. Decide from the task alone.";
|
|
88
72
|
return [intro, ...records.map((record) => recordLine(record, top, now))].join("\n");
|
|
89
73
|
}
|
|
90
|
-
/** One record in full, as JSON: durations in milliseconds, times in epoch milliseconds, context use as a share of the model's window. */
|
|
91
74
|
export function recallRecordText(record) {
|
|
92
75
|
return `Record of the tree under ${record.sessionID}, in project ${record.projectID} (durations in milliseconds, context use as a share of the model's window):\n${JSON.stringify(record, null, 2)}`;
|
|
93
76
|
}
|
|
94
77
|
export const recallMissingText = (sessionID) => `No record of a tree whose top session is ${sessionID} in this project. courier_recall without sessionID lists the records there are.`;
|
|
95
|
-
/** What a lesson is for, which the tool's description and the note's result both say. */
|
|
96
78
|
const NOT_A_RULE = "A lesson that holds every time belongs in AGENTS.md or the skill, which only the person puts there: tell them instead of noting it.";
|
|
97
|
-
/** What courier_note says once the lesson is written: where, and what it replaced. */
|
|
98
79
|
export function noteText(top, lesson, replaced) {
|
|
99
80
|
return [
|
|
100
81
|
`Noted on the record of the tree under ${top} as its ${lessonLine(lesson)}.`,
|
|
@@ -102,29 +83,18 @@ export function noteText(top, lesson, replaced) {
|
|
|
102
83
|
`${RECALL_TOOL} lists it after the tree's facts; a second ${NOTE_TOOL} replaces it. ${NOT_A_RULE}`,
|
|
103
84
|
].join(" ");
|
|
104
85
|
}
|
|
105
|
-
/** What courier_note says when the memory option is off. */
|
|
106
86
|
export const NOTE_OFF = "Not noted: the plugin's memory option is false, so no record of a session tree is kept in this project to write a lesson on.";
|
|
107
87
|
export const noteLeafRefusal = (depth, maxDepth) => `Not noted: this session is a leaf, at depth ${depth} of a session tree that goes at most ${maxDepth} deep (maxDepth). A lesson is ` +
|
|
108
88
|
"written by the session that verified the tree, the top or one that started sessions; put what you learned in your report instead.";
|
|
109
|
-
/** What a lesson holds, which the empty and the long refusal both ask for. */
|
|
110
89
|
const ONE_LINE = "what the next orchestrator in this project should know about running a tree like this one, with the job it comes from and the reason";
|
|
111
90
|
export const NOTE_EMPTY = `Not noted: text is empty. Write one line, ${ONE_LINE}, or nothing.`;
|
|
112
|
-
/** The refusal of a lesson longer than `LESSON_MAX` as one line: nothing is written, and the earlier lesson stays. */
|
|
113
91
|
export const noteLongRefusal = (length) => `Not noted: the lesson is ${length} characters, at most ${LESSON_MAX}; write it shorter. Nothing was written, and the earlier lesson, ` +
|
|
114
92
|
`if there was one, stays. Keep one line, ${ONE_LINE}, and leave out what the record's facts already say.`;
|
|
115
93
|
export const noteUnrecordedRefusal = (top) => `Not noted: the tree under ${top} has no record in this project to write a lesson on. A record is written by a ${RECALL_TOOL} call ` +
|
|
116
94
|
"and as each turn in the tree ends, so call it once first, or note once a session you started has reported.";
|
|
117
|
-
// The lessons entry: what a spawned session's system prompt says of the project's earlier trees.
|
|
118
|
-
/** The key of the session instruction entry carrying the project's lessons, `<context key="lessons">`. */
|
|
119
95
|
export const LESSONS_KEY = "lessons";
|
|
120
|
-
/** How every lessons entry begins. */
|
|
121
96
|
export const LESSONS_PREFIX = "opencode-courier lessons:";
|
|
122
|
-
/** How many lessons the entry carries: the records' order, pinned first, then newest first. */
|
|
123
97
|
export const LESSONS_MAX = 5;
|
|
124
|
-
/**
|
|
125
|
-
* The lessons entry: the lessons of the project's records as courier_recall lists them, pinned first and then newest first,
|
|
126
|
-
* at most `LESSONS_MAX`, each with its tree's title; undefined while no record holds one.
|
|
127
|
-
*/
|
|
128
98
|
export function lessonsEntry(records) {
|
|
129
99
|
const lessons = records.filter((record) => record.lesson).slice(0, LESSONS_MAX);
|
|
130
100
|
if (!lessons.length)
|
package/dist/memory/tools.d.ts
CHANGED
|
@@ -7,10 +7,8 @@ export declare const RecallInput: Schema.Struct<{
|
|
|
7
7
|
export declare const NoteInput: Schema.Struct<{
|
|
8
8
|
readonly text: Schema.String;
|
|
9
9
|
}>;
|
|
10
|
-
/** What the memory's tools run on: one instance's ports and the process's shared state. */
|
|
11
10
|
export interface MemoryToolPorts {
|
|
12
11
|
readonly memory: MemoryPorts;
|
|
13
12
|
readonly state: Pick<MemoryState, "tops">;
|
|
14
13
|
}
|
|
15
|
-
/** Adds `courier_recall` and `courier_note`, direct tools rather than code-mode ones. */
|
|
16
14
|
export declare function addMemoryTools(tools: ToolEditor, ports: MemoryToolPorts): void;
|
package/dist/memory/tools.js
CHANGED
|
@@ -15,7 +15,6 @@ export const NoteInput = Schema.Struct({
|
|
|
15
15
|
"with the job it comes from and the reason.",
|
|
16
16
|
}),
|
|
17
17
|
});
|
|
18
|
-
/** Adds `courier_recall` and `courier_note`, direct tools rather than code-mode ones. */
|
|
19
18
|
export function addMemoryTools(tools, ports) {
|
|
20
19
|
tools.add({
|
|
21
20
|
name: RECALL_TOOL,
|
|
@@ -56,7 +55,6 @@ export function addMemoryTools(tools, ports) {
|
|
|
56
55
|
input: NoteInput,
|
|
57
56
|
execute: async (input, context) => {
|
|
58
57
|
const result = await note(ports.memory, context.sessionID, input).catch(rethrow(NOTE_TOOL));
|
|
59
|
-
// A refusal is the tool's failure, as courier_spawn's are, so the step ends on it and the model reads why.
|
|
60
58
|
const refuse = (text) => describeFailure(NOTE_TOOL, new Error(text));
|
|
61
59
|
switch (result.kind) {
|
|
62
60
|
case "noted": {
|
package/dist/memory/tui.d.ts
CHANGED
|
@@ -1,50 +1,26 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/tui";
|
|
2
2
|
import { type Forgotten, type MemoryLesson, type MemoryPart, type MemoryReads, type MemoryRecord, type Pinned } from "./rpc.js";
|
|
3
3
|
type Context = Plugin.Context;
|
|
4
|
-
/** The command that opens the memory view: `/courier memory`, or the palette. */
|
|
5
4
|
export declare const MEMORY_COMMAND = "courier.memory";
|
|
6
|
-
/** The memory view's line of the `/courier` command's usage. */
|
|
7
5
|
export declare const MEMORY_USAGE = "/courier memory lists this project's tree records, to read, pin or forget";
|
|
8
|
-
/** What the view says, in a toast, with the memory option off in the project asked. */
|
|
9
6
|
export declare const MEMORY_OFF = "Memory is off in this project: the plugin's memory option is false, so no record of a session tree is kept or listed here.";
|
|
10
|
-
/** What the view says, in a toast, when the project has no record: when none was written yet, or the last was forgotten. */
|
|
11
7
|
export declare const NO_RECORDS = "No record of a session tree in this project: one is written as each turn in a tree ends, and kept 90 days.";
|
|
12
|
-
/** The most characters of a lesson a list row's line under it shows; the record in full has it whole. */
|
|
13
8
|
export declare const LESSON_LINE_MAX = 160;
|
|
14
|
-
/** The most characters of a title a list row shows, so that "(this session)" after it fits the host's 61. */
|
|
15
9
|
export declare const ROW_TITLE_MAX = 46;
|
|
16
|
-
/** The group of the records moved on the current local day, over the others' local dates, as the host's Sessions dialog has it. */
|
|
17
10
|
export declare const TODAY = "Today";
|
|
18
|
-
/** The group of the records the person pinned, above the days, shown only while there is one. */
|
|
19
11
|
export declare const PINNED = "Pinned";
|
|
20
|
-
/** The widest a row of the lesson in a record in full is; a longer lesson wraps over rows. */
|
|
21
12
|
export declare const LESSON_ROW = 90;
|
|
22
|
-
/** A record's name, as its rows and confirms show it: its title on one line, else its top session. */
|
|
23
13
|
export declare const nameOf: (record: MemoryRecord) => string;
|
|
24
|
-
|
|
25
|
-
export declare function partsCounted(parts: ReadonlyArray<MemoryPart>): string;
|
|
26
|
-
/** The parts' costs summed, when any is known. */
|
|
14
|
+
export declare function partsCounted(all: ReadonlyArray<MemoryPart>): string;
|
|
27
15
|
export declare function costOf(record: MemoryRecord): number | undefined;
|
|
28
|
-
/** A list row's title: the tree's title, its top session's id without one; the open session's own tree says so. */
|
|
29
16
|
export declare function recordTitle(record: MemoryRecord, open?: string): string;
|
|
30
|
-
/**
|
|
31
|
-
* A list row's group: `Pinned` for a pinned record, `Today` for a tree last moved on the local day of `now`, else that
|
|
32
|
-
* local date; the RPC's order, pinned first and then by the last move, keeps the groups in that order.
|
|
33
|
-
*/
|
|
34
17
|
export declare function recordCategory(record: MemoryRecord, now?: number): string;
|
|
35
|
-
/** How often other trees' courier_recall calls listed the record and opened it in full: "shown 7 times, opened once"; "shown 7 times" alone when none opened it. */
|
|
36
18
|
export declare function readsLine(reads: MemoryReads): string;
|
|
37
|
-
/** A lesson as the view shows it: dated on the local day, from which depth when a session below the top wrote it, and its text. */
|
|
38
19
|
export declare function lessonHeading(lesson: MemoryLesson): string;
|
|
39
|
-
/** A list row's description: its parts by status, how long the tree ran, what its parts cost, that it is not finished when it is not, and how often other trees read it. */
|
|
40
20
|
export declare function recordDescription(record: MemoryRecord): string;
|
|
41
|
-
/** The lines under a list row: the lesson, dated and from which depth, clipped; none without a lesson. */
|
|
42
21
|
export declare function recordDetails(record: MemoryRecord): string[];
|
|
43
|
-
/** One part as the record in full describes it: its status, depth, isolation, group, duration, cost, context use and spawns refused. */
|
|
44
22
|
export declare function partLine(part: MemoryPart): string;
|
|
45
|
-
/** `text` in rows of at most `width` characters, broken at spaces where it can be. */
|
|
46
23
|
export declare function wrap(text: string, width: number): string[];
|
|
47
|
-
/** A row of a record in full: a fact of the tree, one of its parts, or a row of its lesson (0 its heading), which `x` forgets alone. */
|
|
48
24
|
export type Line = {
|
|
49
25
|
readonly kind: "fact";
|
|
50
26
|
readonly key: string;
|
|
@@ -55,27 +31,17 @@ export type Line = {
|
|
|
55
31
|
readonly kind: "lesson";
|
|
56
32
|
readonly row: number;
|
|
57
33
|
};
|
|
58
|
-
/** Where the cursor goes in a record in full after its lesson is forgotten: the lesson's first row, or the row that replaced it. */
|
|
59
34
|
export declare const lessonLine: (record: MemoryRecord) => Line;
|
|
60
|
-
/** The records the list's cursor tries, in order, after one is forgotten, kept or left: it, unless forgotten, then those after it, then those before it, nearest first. */
|
|
61
35
|
export declare function nearby(sessionIDs: ReadonlyArray<string>, sessionID: string, forgotten: boolean): string[];
|
|
62
|
-
/** The rows of a record in full, in three categories: the tree's facts, its parts and its lesson. */
|
|
63
36
|
export declare function detailOptions(record: MemoryRecord): Array<{
|
|
64
37
|
title: string;
|
|
65
38
|
value: Line;
|
|
66
39
|
category: string;
|
|
67
40
|
description?: string;
|
|
68
41
|
}>;
|
|
69
|
-
/** What the confirm asks before a forget: the record named, with the local day it last moved, and what goes and what stays. */
|
|
70
42
|
export declare function forgetMessage(record: MemoryRecord, lesson: boolean): string;
|
|
71
|
-
/** The toast after a forget, as the server reports it. */
|
|
72
43
|
export declare function forgottenLine(record: MemoryRecord, outcome: Forgotten["outcome"]): string;
|
|
73
|
-
/** The toast after a pin, as the server reports it. */
|
|
74
44
|
export declare function pinnedLine(record: MemoryRecord, outcome: Pinned["outcome"]): string;
|
|
75
|
-
/**
|
|
76
|
-
* Adds the memory view: the project's records (the open session's location, else the TUI's, else the server's default) under
|
|
77
|
-
* `Pinned`, `Today` and the days they last moved; `enter` shows one in full, `p` pins or unpins one, `x` forgets one or its lesson after a confirm.
|
|
78
|
-
*/
|
|
79
45
|
export declare function addMemoryView(ctx: Context): {
|
|
80
46
|
open: () => Promise<void>;
|
|
81
47
|
};
|
package/dist/person/events.d.ts
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import type { EventModule } from "../platform/events.js";
|
|
2
2
|
import { type PermissionAsked, type PermissionReplied } from "./permission.js";
|
|
3
3
|
import { type PersonPorts, type PersonState } from "./ports.js";
|
|
4
|
-
/**
|
|
5
|
-
* Tells the session at the top, the parent of a child or the session a chain of children started
|
|
6
|
-
* from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
|
|
7
|
-
*/
|
|
8
4
|
export declare function reportAsked(ports: PersonPorts, state: PersonState, event: PermissionAsked): Promise<string[]>;
|
|
9
|
-
/**
|
|
10
|
-
* Tells the session told about a request that it was answered some other way, so it does not pass on a stale
|
|
11
|
-
* question; requests answered through courier_answer are skipped. Sent with `resume: false`, so it starts no turn.
|
|
12
|
-
*/
|
|
13
5
|
export declare function reportReplied(ports: PersonPorts, state: PersonState, event: PermissionReplied): Promise<string[]>;
|
|
14
|
-
/**
|
|
15
|
-
* Relays the requests that spawned sessions already wait on, in every loaded location, which the
|
|
16
|
-
* event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
|
|
17
|
-
*/
|
|
18
6
|
export declare function relayPending(ports: PersonPorts, state: PersonState): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* Tells the top session of a spawned child's pending permission or form, relays the requests already
|
|
21
|
-
* pending on every (re)subscribe, and tells the question relay when events are followed and when not.
|
|
22
|
-
*/
|
|
23
7
|
export declare function personEvents(state: PersonState): EventModule<PersonPorts>;
|
package/dist/person/events.js
CHANGED
|
@@ -6,21 +6,15 @@ import { listEverywhere } from "./permission.js";
|
|
|
6
6
|
import { topOf } from "./ports.js";
|
|
7
7
|
import { eventsFollowed, eventsLeft, formShown, formsMayHaveBeenMissed, locationClosing } from "./question/index.js";
|
|
8
8
|
import { permissionNotice, permissionSettledNotice } from "./text.js";
|
|
9
|
-
/**
|
|
10
|
-
* Tells the session at the top, the parent of a child or the session a chain of children started
|
|
11
|
-
* from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
|
|
12
|
-
*/
|
|
13
9
|
export async function reportAsked(ports, state, event) {
|
|
14
10
|
if (!claim(state.seen, event.id))
|
|
15
11
|
return [];
|
|
16
12
|
const request = event.data;
|
|
17
13
|
const chain = await lineage(ports.storage, request.sessionID);
|
|
18
|
-
// Claimed only now, after the lookup: a request answered meanwhile is in `answered`, and one
|
|
19
|
-
// already relayed, from the event or from the pending list, is in `waiting`.
|
|
20
14
|
if (!chain.length || state.answered.has(request.id) || !claim(state.waiting, request.id))
|
|
21
15
|
return [];
|
|
22
16
|
const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
|
|
23
|
-
//
|
|
17
|
+
// a native child's request shows in its native root's view (api-notes)
|
|
24
18
|
const native = nativeRootOf(chain) === topOf(chain);
|
|
25
19
|
await ports.session.synthetic({
|
|
26
20
|
sessionID: topOf(chain),
|
|
@@ -35,10 +29,6 @@ export async function reportAsked(ports, state, event) {
|
|
|
35
29
|
});
|
|
36
30
|
return [topOf(chain)];
|
|
37
31
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Tells the session told about a request that it was answered some other way, so it does not pass on a stale
|
|
40
|
-
* question; requests answered through courier_answer are skipped. Sent with `resume: false`, so it starts no turn.
|
|
41
|
-
*/
|
|
42
32
|
export async function reportReplied(ports, state, event) {
|
|
43
33
|
if (!claim(state.seen, event.id))
|
|
44
34
|
return [];
|
|
@@ -60,57 +50,40 @@ export async function reportReplied(ports, state, event) {
|
|
|
60
50
|
});
|
|
61
51
|
return [topOf(chain)];
|
|
62
52
|
}
|
|
63
|
-
|
|
64
|
-
* Relays the requests that spawned sessions already wait on, in every loaded location, which the
|
|
65
|
-
* event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
|
|
66
|
-
*/
|
|
53
|
+
// the event stream does not replay requests asked before it subscribed
|
|
67
54
|
export async function relayPending(ports, state) {
|
|
68
55
|
const sessions = new Set((await allEntries(ports.storage)).map((entry) => entry.sessionID));
|
|
69
56
|
const domains = [...ports.permissions()];
|
|
70
|
-
// Each request is claimed before its notice goes out, so relaying them all at once tells each
|
|
71
|
-
// once, however many locations list it; a location that cannot be read lists none.
|
|
72
57
|
const relay = async (sessionID) => {
|
|
73
58
|
const listed = (await listEverywhere(domains, sessionID)).flatMap((found) => found.requests);
|
|
74
59
|
await Promise.all(listed.map((request) => reportAsked(ports, state, { id: `pending:${request.id}`, data: request })));
|
|
75
60
|
};
|
|
76
61
|
await Promise.all([...sessions].map(relay));
|
|
77
62
|
}
|
|
78
|
-
/** What `routePerson` dispatches on. */
|
|
79
63
|
const PERSON_TYPES = ["permission.asked", "permission.replied", "form.created", "form.replied", "form.cancelled", "location.shutdown"];
|
|
80
|
-
/** The events of what a spawned session asks the person: permission requests, forms, and locations closing on them. */
|
|
81
64
|
function routePerson(ports, state, event) {
|
|
82
65
|
if (event.type === "permission.asked")
|
|
83
66
|
return reportAsked(ports, state, event);
|
|
84
67
|
if (event.type === "permission.replied")
|
|
85
68
|
return reportReplied(ports, state, event);
|
|
86
69
|
if (event.type === "form.created") {
|
|
87
|
-
// Not claimed: the hub's other subscription, or another copy's watcher, may resolve the same
|
|
88
|
-
// waiting call, which is harmless.
|
|
89
70
|
formShown(event);
|
|
90
71
|
return reportForm(ports, state, event);
|
|
91
72
|
}
|
|
92
73
|
if (event.type === "form.replied" || event.type === "form.cancelled")
|
|
93
74
|
return reportFormSettled(ports, state, event);
|
|
94
|
-
//
|
|
95
|
-
// The event's location is optional; without one, the shutdown counts for every location.
|
|
75
|
+
// location.shutdown may name no location: it then counts for all
|
|
96
76
|
if (event.type === "location.shutdown") {
|
|
97
77
|
const directory = event.location?.directory;
|
|
98
78
|
locationClosing(ports.now(), typeof directory === "string" ? directory : undefined);
|
|
99
79
|
}
|
|
100
80
|
return undefined;
|
|
101
81
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Tells the top session of a spawned child's pending permission or form, relays the requests already
|
|
104
|
-
* pending on every (re)subscribe, and tells the question relay when events are followed and when not.
|
|
105
|
-
*/
|
|
106
82
|
export function personEvents(state) {
|
|
107
83
|
return {
|
|
108
84
|
types: PERSON_TYPES,
|
|
109
85
|
handle: (ports, event) => routePerson(ports, state, event),
|
|
110
|
-
// Question forms shown while no instance followed the events were not seen: released now,
|
|
111
|
-
// and again once the stream is surely connected.
|
|
112
86
|
followed: () => (eventsFollowed() ? formsMayHaveBeenMissed : undefined),
|
|
113
|
-
// Alongside the new subscription; a request both relays see is relayed once.
|
|
114
87
|
subscribed: (ports) => relayPending(ports, state).catch((error) => ports.log(`courier watch: could not relay pending requests: ${String(error)}`)),
|
|
115
88
|
left: eventsLeft,
|
|
116
89
|
};
|
package/dist/person/form.d.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { type ShownForm } from "../tree/index.js";
|
|
2
2
|
import { type Form, type FormsShown, type PersonPorts, type PersonState } from "./ports.js";
|
|
3
|
-
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
4
3
|
export declare const QUESTION_FORM = "question";
|
|
5
|
-
/** The part of OpenCode's `form.created` event the notice is made from. */
|
|
6
4
|
export interface FormCreated {
|
|
7
5
|
readonly id: string;
|
|
8
6
|
readonly data: {
|
|
9
7
|
readonly form: Form;
|
|
10
8
|
};
|
|
11
9
|
}
|
|
12
|
-
/** OpenCode's `form.replied` or `form.cancelled` event: the form is no longer shown. */
|
|
13
10
|
export interface FormSettled {
|
|
14
11
|
readonly id: string;
|
|
15
12
|
readonly type: string;
|
|
@@ -18,12 +15,6 @@ export interface FormSettled {
|
|
|
18
15
|
readonly sessionID: string;
|
|
19
16
|
};
|
|
20
17
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Tells the top session about a form OpenCode shows in a spawned session, such as web search asking
|
|
23
|
-
* for its provider, which only the person can answer; question forms are left to the question relay.
|
|
24
|
-
*/
|
|
25
18
|
export declare function reportForm(ports: PersonPorts, state: PersonState, event: FormCreated): Promise<string[]>;
|
|
26
|
-
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
27
19
|
export declare function reportFormSettled(ports: PersonPorts, state: PersonState, event: FormSettled): Promise<string[]>;
|
|
28
|
-
/** The forms a session shows now, oldest first, as the tree's `forms` port lists them. */
|
|
29
20
|
export declare const formsShown: (shown: FormsShown, sessionID: string) => ShownForm[];
|
package/dist/person/form.js
CHANGED
|
@@ -3,12 +3,7 @@ import { claim, SEEN_MAX, setBounded } from "../platform/bounded.js";
|
|
|
3
3
|
import { lineage, nativeRootOf } from "../tree/index.js";
|
|
4
4
|
import { topOf } from "./ports.js";
|
|
5
5
|
import { formNotice, formSettledNotice, kindOf } from "./text.js";
|
|
6
|
-
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
7
6
|
export const QUESTION_FORM = "question";
|
|
8
|
-
/**
|
|
9
|
-
* Tells the top session about a form OpenCode shows in a spawned session, such as web search asking
|
|
10
|
-
* for its provider, which only the person can answer; question forms are left to the question relay.
|
|
11
|
-
*/
|
|
12
7
|
export async function reportForm(ports, state, event) {
|
|
13
8
|
if (!claim(state.seen, event.id))
|
|
14
9
|
return [];
|
|
@@ -18,7 +13,6 @@ export async function reportForm(ports, state, event) {
|
|
|
18
13
|
if (!state.forms.settled.has(form.id))
|
|
19
14
|
setBounded(state.shown, form.id, { sessionID: form.sessionID, title: form.title }, SEEN_MAX);
|
|
20
15
|
const chain = await lineage(ports.storage, form.sessionID);
|
|
21
|
-
// As with a permission request, claimed after the lookup, so a form settled meanwhile is not told.
|
|
22
16
|
if (!chain.length || state.forms.settled.has(form.id) || state.forms.told.has(form.id))
|
|
23
17
|
return [];
|
|
24
18
|
const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
|
|
@@ -38,7 +32,6 @@ export async function reportForm(ports, state, event) {
|
|
|
38
32
|
await telling;
|
|
39
33
|
return [topOf(chain)];
|
|
40
34
|
}
|
|
41
|
-
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
42
35
|
export async function reportFormSettled(ports, state, event) {
|
|
43
36
|
if (!claim(state.seen, event.id))
|
|
44
37
|
return [];
|
|
@@ -51,8 +44,6 @@ export async function reportFormSettled(ports, state, event) {
|
|
|
51
44
|
}
|
|
52
45
|
state.forms.told.delete(id);
|
|
53
46
|
const settled = event.type === "form.replied" ? "answered" : "cancelled";
|
|
54
|
-
// After the notice that the form is shown, which another instance may still be sending, so the
|
|
55
|
-
// settling never arrives first; and not at all when that notice did not go out.
|
|
56
47
|
const [chain, told] = await Promise.all([lineage(ports.storage, sessionID), telling.then(() => true, () => false)]);
|
|
57
48
|
if (!chain.length || !told)
|
|
58
49
|
return [];
|
|
@@ -65,5 +56,4 @@ export async function reportFormSettled(ports, state, event) {
|
|
|
65
56
|
});
|
|
66
57
|
return [topOf(chain)];
|
|
67
58
|
}
|
|
68
|
-
/** The forms a session shows now, oldest first, as the tree's `forms` port lists them. */
|
|
69
59
|
export const formsShown = (shown, sessionID) => [...shown].flatMap(([id, form]) => (form.sessionID === sessionID ? [{ id, title: form.title }] : []));
|
package/dist/person/index.d.ts
CHANGED
|
@@ -12,7 +12,5 @@ export type { QuestionPorts } from "./question/index.js";
|
|
|
12
12
|
export { addPersonTools } from "./tools.js";
|
|
13
13
|
export type { PersonToolPorts } from "./tools.js";
|
|
14
14
|
type Satisfies<T extends U, U> = T;
|
|
15
|
-
/** The tree declares the question shape its `Pending` needs; this fails to compile once `Prompt` and it differ. */
|
|
16
15
|
export type PromptMatchesPending = [Satisfies<Prompt, PendingPrompt>, Satisfies<PendingPrompt, Prompt>];
|
|
17
|
-
/** The watcher's ports are the ones the person events run on; this fails to compile once `WatchPorts` stops satisfying `PersonPorts`. */
|
|
18
16
|
export type WatchPortsArePersonPorts = Satisfies<WatchPorts, PersonPorts>;
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin";
|
|
2
2
|
import { type Pending, type RosterStorage } from "../tree/index.js";
|
|
3
|
-
/** The answers to a permission request, as OpenCode's own prompt offers them. */
|
|
4
3
|
export declare const REPLIES: readonly ["once", "always", "reject"];
|
|
5
4
|
export type Reply = (typeof REPLIES)[number];
|
|
6
|
-
/**
|
|
7
|
-
* What courier_answer passes on: `once`, which OpenCode first asks the person about in the caller's
|
|
8
|
-
* session, or `reject`. Never `always`: the plugin grants no standing rule to the caller or the child.
|
|
9
|
-
*/
|
|
10
5
|
export declare const PROPOSALS: readonly ["once", "reject"];
|
|
11
6
|
export type Proposal = (typeof PROPOSALS)[number];
|
|
12
|
-
/** A permission request, as OpenCode's `permission.asked` event carries it. */
|
|
13
7
|
export interface PermissionRequest {
|
|
14
8
|
readonly id: string;
|
|
15
9
|
readonly sessionID: string;
|
|
@@ -19,10 +13,6 @@ export interface PermissionRequest {
|
|
|
19
13
|
readonly message?: string;
|
|
20
14
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
21
15
|
}
|
|
22
|
-
/**
|
|
23
|
-
* What answering a permission request did; `answered` is false when no location loaded here held the
|
|
24
|
-
* request: answered already, no longer waited for, or waiting in another server on the same data dir.
|
|
25
|
-
*/
|
|
26
16
|
export interface PermissionAnswered {
|
|
27
17
|
readonly sessionID: string;
|
|
28
18
|
readonly requestID: string;
|
|
@@ -30,16 +20,12 @@ export interface PermissionAnswered {
|
|
|
30
20
|
readonly answered: boolean;
|
|
31
21
|
}
|
|
32
22
|
type Context = Plugin.Context;
|
|
33
|
-
/** One location's pending permission requests. OpenCode keeps them per location, so an isolated child's are in its worktree's. */
|
|
34
23
|
export type Permissions = Pick<Context["permission"], "list" | "reply">;
|
|
35
|
-
/** OpenCode's check of a session's permission rules, which asks the person in that session when they say so. */
|
|
36
24
|
export type Assert = Context["permission"]["assert"];
|
|
37
|
-
/** The part of OpenCode's `permission.asked` event the notice is made from. */
|
|
38
25
|
export interface PermissionAsked {
|
|
39
26
|
readonly id: string;
|
|
40
27
|
readonly data: PermissionRequest;
|
|
41
28
|
}
|
|
42
|
-
/** The part of OpenCode's `permission.replied` event. */
|
|
43
29
|
export interface PermissionReplied {
|
|
44
30
|
readonly id: string;
|
|
45
31
|
readonly data: {
|
|
@@ -48,19 +34,12 @@ export interface PermissionReplied {
|
|
|
48
34
|
readonly reply: Reply;
|
|
49
35
|
};
|
|
50
36
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Shared by every plugin instance in the process, like the handled event ids: the requests whose
|
|
53
|
-
* notice went to a parent and that courier_answer has not answered yet.
|
|
54
|
-
*/
|
|
55
37
|
export type Waiting = Set<string>;
|
|
56
38
|
export interface AnswerPorts {
|
|
57
39
|
readonly storage: RosterStorage;
|
|
58
|
-
/** The permission domains of every loaded location; the request is answered where it is pending. */
|
|
59
40
|
readonly permissions: () => Iterable<Permissions>;
|
|
60
|
-
/** The caller's location's check, which prompts the person in the caller's session. */
|
|
61
41
|
readonly assert: Assert;
|
|
62
42
|
}
|
|
63
|
-
/** The courier_answer call a proposal comes from, which OpenCode's prompt in the caller's session is bound to. */
|
|
64
43
|
export interface AnswerCall {
|
|
65
44
|
readonly messageID: string;
|
|
66
45
|
readonly callID: string;
|
|
@@ -72,7 +51,6 @@ export interface AnswerInput {
|
|
|
72
51
|
readonly message?: string;
|
|
73
52
|
readonly call?: AnswerCall;
|
|
74
53
|
}
|
|
75
|
-
/** A session's pending requests in every location, with the domain holding each; a location that cannot be read gives its error. */
|
|
76
54
|
export declare function listEverywhere<D extends Pick<Permissions, "list">>(permissions: Iterable<D>, sessionID: string): Promise<({
|
|
77
55
|
domain: D;
|
|
78
56
|
requests: never[];
|
|
@@ -81,26 +59,15 @@ export declare function listEverywhere<D extends Pick<Permissions, "list">>(perm
|
|
|
81
59
|
domain: D;
|
|
82
60
|
requests: import("@opencode/client").PermissionRequest[];
|
|
83
61
|
})[]>;
|
|
84
|
-
/** Whether a session waits on a permission request of its own, in any loaded location. */
|
|
85
62
|
export declare function asksPermission(ports: {
|
|
86
63
|
readonly permissions: () => Iterable<Pick<Permissions, "list">>;
|
|
87
64
|
}, sessionID: string): Promise<boolean>;
|
|
88
|
-
/**
|
|
89
|
-
* Whether `permission.assert` failed because the person rejected the caller's prompt without a reason,
|
|
90
|
-
* which OpenCode ends the caller's step on; the tool rethrows it as it is, and the child's request waits on.
|
|
91
|
-
*/
|
|
92
65
|
export declare const isDeclined: (error: unknown) => boolean;
|
|
93
|
-
/**
|
|
94
|
-
* Answers a permission request of a session started, directly or through others, from the caller, which must be the top
|
|
95
|
-
* session, a proposed `once` only once OpenCode allowed it there; `answered` is false when no location loaded here holds it.
|
|
96
|
-
*/
|
|
97
66
|
export declare function answer(ports: AnswerPorts, waiting: Waiting, callerID: string, input: AnswerInput): Promise<PermissionAnswered>;
|
|
98
|
-
/** The child's request a prompt of courier_answer's asks about, read from the metadata `askCaller` gives it; undefined for any other request. */
|
|
99
67
|
export declare function passedOn(metadata: Readonly<Record<string, unknown>> | undefined): {
|
|
100
68
|
sessionID: string;
|
|
101
69
|
requestID: string;
|
|
102
70
|
} | undefined;
|
|
103
|
-
/** The requests a session waits on of its own, in whichever location holds them, each with the child's request it passes on when courier_answer asks it. */
|
|
104
71
|
export declare function ownRequests(permissions: Iterable<Permissions>, sessionID: string): Promise<{
|
|
105
72
|
request: PermissionRequest;
|
|
106
73
|
passesOn: {
|
|
@@ -108,6 +75,5 @@ export declare function ownRequests(permissions: Iterable<Permissions>, sessionI
|
|
|
108
75
|
requestID: string;
|
|
109
76
|
} | undefined;
|
|
110
77
|
}[]>;
|
|
111
|
-
/** The requests a session waits on, in whichever location holds them, for courier_children. */
|
|
112
78
|
export declare function pendingOf(permissions: Iterable<Permissions>, sessionID: string): Promise<Pending[]>;
|
|
113
79
|
export {};
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { describeFailure } from "../kernel/protocol.js";
|
|
2
2
|
import { REJECTED, refusedText } from "./text.js";
|
|
3
3
|
import { answeringTop } from "../tree/index.js";
|
|
4
|
-
/** The answers to a permission request, as OpenCode's own prompt offers them. */
|
|
5
4
|
export const REPLIES = ["once", "always", "reject"];
|
|
6
|
-
/**
|
|
7
|
-
* What courier_answer passes on: `once`, which OpenCode first asks the person about in the caller's
|
|
8
|
-
* session, or `reject`. Never `always`: the plugin grants no standing rule to the caller or the child.
|
|
9
|
-
*/
|
|
10
5
|
export const PROPOSALS = ["once", "reject"];
|
|
11
|
-
/** A session's pending requests in every location, with the domain holding each; a location that cannot be read gives its error. */
|
|
12
6
|
export function listEverywhere(permissions, sessionID) {
|
|
13
7
|
return Promise.all([...permissions].map((domain) => domain.list({ sessionID }).then((requests) => ({ domain, requests: requests.filter((request) => request.sessionID === sessionID) }), (error) => ({ domain, requests: [], error }))));
|
|
14
8
|
}
|
|
15
|
-
/** Whether a session waits on a permission request of its own, in any loaded location. */
|
|
16
9
|
export async function asksPermission(ports, sessionID) {
|
|
17
10
|
return (await listEverywhere([...ports.permissions()], sessionID)).some((found) => found.requests.length > 0);
|
|
18
11
|
}
|
|
19
|
-
/** The location holding a pending request, and the request; undefined when none does, and an error when one could not be read. */
|
|
20
12
|
async function locate(ports, sessionID, requestID) {
|
|
21
13
|
const listed = await listEverywhere(ports.permissions(), sessionID);
|
|
22
14
|
for (const { domain, requests } of listed) {
|
|
@@ -30,12 +22,8 @@ async function locate(ports, sessionID, requestID) {
|
|
|
30
22
|
return undefined;
|
|
31
23
|
}
|
|
32
24
|
const tagOf = (value) => value?._tag;
|
|
33
|
-
/**
|
|
34
|
-
* Whether `permission.assert` failed because the person rejected the caller's prompt without a reason,
|
|
35
|
-
* which OpenCode ends the caller's step on; the tool rethrows it as it is, and the child's request waits on.
|
|
36
|
-
*/
|
|
37
25
|
export const isDeclined = (error) => tagOf(error) === "Permission.DeclinedError";
|
|
38
|
-
|
|
26
|
+
// the host's Tool.Error message is the deny rule's or the feedback (api-notes)
|
|
39
27
|
function refusalOf(error) {
|
|
40
28
|
if (tagOf(error) !== "Tool.Error")
|
|
41
29
|
return undefined;
|
|
@@ -45,12 +33,9 @@ function refusalOf(error) {
|
|
|
45
33
|
return undefined;
|
|
46
34
|
return typeof message === "string" && message ? message : "not allowed";
|
|
47
35
|
}
|
|
48
|
-
/**
|
|
49
|
-
* OpenCode's check of the child's request under the caller's rules, which asks the person in the caller's session, with the
|
|
50
|
-
* request's own action and resources and nothing to save: the reason when refused with one, nothing when allowed, else thrown.
|
|
51
|
-
*/
|
|
52
36
|
async function askCaller(ports, callerID, request, call) {
|
|
53
37
|
try {
|
|
38
|
+
// no save: always would answer the other requests it covers (api-notes)
|
|
54
39
|
await ports.assert({
|
|
55
40
|
sessionID: callerID,
|
|
56
41
|
action: request.action,
|
|
@@ -67,17 +52,14 @@ async function askCaller(ports, callerID, request, call) {
|
|
|
67
52
|
return reason;
|
|
68
53
|
}
|
|
69
54
|
}
|
|
70
|
-
/** The reply as a proposal courier_answer passes on; anything else, `always` in particular, is refused. */
|
|
71
55
|
function proposalOf(reply) {
|
|
72
56
|
if (PROPOSALS.includes(reply))
|
|
73
57
|
return reply;
|
|
74
58
|
const always = reply === "always" ? ": courier_answer grants no standing rule; the person grants one in OpenCode's own prompt, in the asking session or the asks inbox" : "";
|
|
75
59
|
throw new Error(`reply must be once or reject, not "${reply}"${always}.`);
|
|
76
60
|
}
|
|
77
|
-
/** Replies where the request is pending; a reply that fails leaves the request waiting, unless it no longer does. */
|
|
78
61
|
async function passOn(ports, waiting, found, reply) {
|
|
79
62
|
const { sessionID, requestID, decision, message } = reply;
|
|
80
|
-
// Off the waiting list first, so the reply event this answer causes does not come back as a notice.
|
|
81
63
|
waiting.delete(requestID);
|
|
82
64
|
try {
|
|
83
65
|
await found.domain.reply({ sessionID, requestID, decision, ...(message ? { message } : {}) });
|
|
@@ -90,10 +72,6 @@ async function passOn(ports, waiting, found, reply) {
|
|
|
90
72
|
throw new Error(`${describeFailure("permission.reply", error).message}, but ${requestID} no longer waits, so it was answered, possibly by this call.`);
|
|
91
73
|
}
|
|
92
74
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Answers a permission request of a session started, directly or through others, from the caller, which must be the top
|
|
95
|
-
* session, a proposed `once` only once OpenCode allowed it there; `answered` is false when no location loaded here holds it.
|
|
96
|
-
*/
|
|
97
75
|
export async function answer(ports, waiting, callerID, input) {
|
|
98
76
|
const reply = proposalOf(input.reply);
|
|
99
77
|
const { sessionID, requestID } = input;
|
|
@@ -102,7 +80,6 @@ export async function answer(ports, waiting, callerID, input) {
|
|
|
102
80
|
let refused;
|
|
103
81
|
if (found && reply === "once") {
|
|
104
82
|
refused = await askCaller(ports, callerID, found.request, input.call);
|
|
105
|
-
// Answered meanwhile, in the child's session or the asks inbox, while the person was asked.
|
|
106
83
|
found = await locate(ports, sessionID, requestID);
|
|
107
84
|
}
|
|
108
85
|
if (!found) {
|
|
@@ -115,14 +92,12 @@ export async function answer(ports, waiting, callerID, input) {
|
|
|
115
92
|
throw new Error(refusedText(sessionID, requestID, refused));
|
|
116
93
|
return { sessionID, requestID, reply, answered: true };
|
|
117
94
|
}
|
|
118
|
-
/** The child's request a prompt of courier_answer's asks about, read from the metadata `askCaller` gives it; undefined for any other request. */
|
|
119
95
|
export function passedOn(metadata) {
|
|
120
96
|
const { source, asks, from, requestID } = metadata ?? {};
|
|
121
97
|
if (source !== "courier" || asks !== "permission" || typeof from !== "string" || typeof requestID !== "string")
|
|
122
98
|
return undefined;
|
|
123
99
|
return { sessionID: from, requestID };
|
|
124
100
|
}
|
|
125
|
-
/** The requests a session waits on of its own, in whichever location holds them, each with the child's request it passes on when courier_answer asks it. */
|
|
126
101
|
export async function ownRequests(permissions, sessionID) {
|
|
127
102
|
const found = new Map();
|
|
128
103
|
for (const { requests } of await listEverywhere(permissions, sessionID))
|
|
@@ -130,7 +105,6 @@ export async function ownRequests(permissions, sessionID) {
|
|
|
130
105
|
found.set(request.id, request);
|
|
131
106
|
return [...found.values()].map((request) => ({ request, passesOn: passedOn(request.metadata) }));
|
|
132
107
|
}
|
|
133
|
-
/** The requests a session waits on, in whichever location holds them, for courier_children. */
|
|
134
108
|
export async function pendingOf(permissions, sessionID) {
|
|
135
109
|
const found = new Map();
|
|
136
110
|
for (const { requests } of await listEverywhere(permissions, sessionID))
|