@softov/ahpc 0.2.0 → 0.4.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 +37 -6
- package/dist/src/ahp/fake.js +61 -14
- package/dist/src/ahp/live.d.ts +7 -0
- package/dist/src/ahp/live.js +156 -43
- package/dist/src/ahp/publish.js +13 -0
- package/dist/src/ahp/types.d.ts +20 -0
- package/dist/src/app.js +34 -7
- package/dist/src/blocks.d.ts +4 -74
- package/dist/src/blocks.js +10 -48
- package/dist/src/cli/main.d.ts +1 -1
- package/dist/src/cli/main.js +109 -6
- package/dist/src/connect.d.ts +2 -0
- package/dist/src/connect.js +1 -0
- package/dist/src/control.d.ts +6 -0
- package/dist/src/control.js +140 -10
- package/dist/src/flags.js +4 -1
- package/dist/src/links.d.ts +54 -0
- package/dist/src/links.js +120 -0
- package/dist/src/main.js +14 -2
- package/dist/src/mcp/http.d.ts +10 -0
- package/dist/src/mcp/http.js +110 -3
- package/dist/src/mcp/serve.d.ts +47 -9
- package/dist/src/mcp/serve.js +65 -14
- package/dist/src/mcp/stdio.d.ts +1 -0
- package/dist/src/mcp/stdio.js +7 -1
- package/dist/src/mcp/tools.d.ts +37 -2
- package/dist/src/mcp/tools.js +439 -2
- package/dist/src/resources.d.ts +13 -0
- package/dist/src/resources.js +46 -0
- package/dist/src/screens.js +95 -50
- package/dist/src/state.d.ts +42 -0
- package/dist/src/state.js +80 -1
- package/dist/src/tui.d.ts +3 -1
- package/dist/src/tui.js +29 -4
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +38 -0
- package/dist/src/view/creature.d.ts +0 -12
- package/dist/src/view/creature.js +0 -20
- package/dist/src/view/wire.d.ts +36 -0
- package/dist/src/view/wire.js +196 -0
- package/dist/src/wait.d.ts +11 -0
- package/dist/src/wait.js +15 -2
- package/dist/src/wire.d.ts +70 -0
- package/dist/src/wire.js +194 -0
- package/dist/src/wiretui.d.ts +22 -0
- package/dist/src/wiretui.js +69 -0
- package/package.json +6 -5
- package/dist/src/diff.d.ts +0 -44
- package/dist/src/diff.js +0 -111
- package/dist/src/view/bubble.d.ts +0 -75
- package/dist/src/view/bubble.js +0 -86
- package/dist/src/view/composer.d.ts +0 -64
- package/dist/src/view/composer.js +0 -192
- package/dist/src/view/controls.d.ts +0 -44
- package/dist/src/view/controls.js +0 -49
- package/dist/src/view/details.d.ts +0 -65
- package/dist/src/view/details.js +0 -65
- package/dist/src/view/filediff.d.ts +0 -29
- package/dist/src/view/filediff.js +0 -24
- package/dist/src/view/hitl.d.ts +0 -43
- package/dist/src/view/hitl.js +0 -171
- package/dist/src/view/icons.d.ts +0 -13
- package/dist/src/view/icons.js +0 -71
- package/dist/src/view/picker.d.ts +0 -42
- package/dist/src/view/picker.js +0 -71
- package/dist/src/view/sessionhead.d.ts +0 -41
- package/dist/src/view/sessionhead.js +0 -60
- package/dist/src/view/sessions.d.ts +0 -34
- package/dist/src/view/sessions.js +0 -61
- package/dist/src/view/toolcall.d.ts +0 -27
- package/dist/src/view/toolcall.js +0 -48
- package/dist/src/view/transcript.d.ts +0 -50
- package/dist/src/view/transcript.js +0 -60
package/dist/src/app.js
CHANGED
|
@@ -3,19 +3,15 @@ import { createBag, defineComponent, useApp, useTheme, useStoreSubtree, useStore
|
|
|
3
3
|
import { KeyHints, Row, registerBuiltins } from '@textui/widgets';
|
|
4
4
|
import { CONTROLLER, createController } from './control.js';
|
|
5
5
|
import { fakeHost } from './ahp/fake.js';
|
|
6
|
+
import { hostResources } from './resources.js';
|
|
6
7
|
import { BOOD, BOOD_FLOAT, BOOD_FLOOR, BOOD_INLINE, FOCUS, HOST, HOST_ERROR, INPUT, INPUT_STATUS, OPEN, RUNNING, SCREEN, SESSIONS, SPLIT_AT, SPLIT_DEFAULT, STATUS, WORKSPACE, boodFloor, openSession, workspaceName, } from './state.js';
|
|
7
8
|
import { decodeStatus } from './ahp/status.js';
|
|
8
9
|
import { AutomationsScreen, ChangesScreen, ChatScreen, FilesScreen, HostsScreen, McpScreen, NewAutomationScreen, NewSessionScreen, SessionsScreen, TerminalScreen, SettingsScreen, SkillsScreen, } from './screens.js';
|
|
9
|
-
import { ChatBubble, ReasoningBlock, StreamingText } from '
|
|
10
|
-
import { ChatComposer } from './view/composer.js';
|
|
11
|
-
import { ChatHitl } from './view/hitl.js';
|
|
12
|
-
import { ChatTranscript } from './view/transcript.js';
|
|
10
|
+
import { ChatBubble, ChatComposer, ChatHitl, ChatTranscript, ConnectionBadge, ReasoningBlock, SEND_ID, SessionList, StreamingText, ToolCallRow, } from '@textui/chat';
|
|
13
11
|
import { BoodSprite, Creature, moodOf, pickBood } from './view/creature.js';
|
|
14
12
|
import { ChangesList } from './view/changes.js';
|
|
15
13
|
import { FileList } from './view/files.js';
|
|
16
14
|
import { AutomationList } from './view/automations.js';
|
|
17
|
-
import { ConnectionBadge, SessionList } from './view/sessions.js';
|
|
18
|
-
import { ToolCallRow } from './view/toolcall.js';
|
|
19
15
|
/**
|
|
20
16
|
* A chat client for an agent host.
|
|
21
17
|
*
|
|
@@ -124,6 +120,7 @@ const Header = defineComponent('ChatHeader', () => {
|
|
|
124
120
|
*/
|
|
125
121
|
const Hints = defineComponent('ChatHints', (props) => {
|
|
126
122
|
const theme = useTheme();
|
|
123
|
+
const app = useApp();
|
|
127
124
|
/**
|
|
128
125
|
* Which key the footer names for a newline.
|
|
129
126
|
*
|
|
@@ -153,8 +150,19 @@ const Hints = defineComponent('ChatHints', (props) => {
|
|
|
153
150
|
// it, escape leaves the field; from the transcript, escape leaves the
|
|
154
151
|
// screen - and a hint row that said one of those in both places is wrong
|
|
155
152
|
// half the time.
|
|
156
|
-
const focused = useStoreValue(FOCUS, null);
|
|
153
|
+
const focused = useStoreValue(FOCUS, null) ?? null;
|
|
157
154
|
const composing = focused === 'chat.composer';
|
|
155
|
+
// The control rows under the field are their own place: enter there opens
|
|
156
|
+
// a chip's panel or sends, and "alt+enter newline" is a key for a field
|
|
157
|
+
// the keyboard has left.
|
|
158
|
+
const onChip = focused !== null && focused.startsWith('chat.option.');
|
|
159
|
+
const onSend = focused === SEND_ID;
|
|
160
|
+
// A panel over the screen - a chip's picker, the palette - holds the
|
|
161
|
+
// keyboard, and the keys are the panel's until it closes. Read off the
|
|
162
|
+
// layers when the focus moves, which a panel that traps it does on the way
|
|
163
|
+
// in and on the way out; the layers themselves are not something a
|
|
164
|
+
// component can subscribe to.
|
|
165
|
+
const inPanel = app.layers.entries().some((entry) => entry.trapFocus === true);
|
|
158
166
|
// A question is not a confirmation, and the keys are not the same either.
|
|
159
167
|
// Offering "a approve" over an elicitation is the same mistake as rendering
|
|
160
168
|
// one as the other, made in the one row that is supposed to explain it.
|
|
@@ -174,6 +182,22 @@ const Hints = defineComponent('ChatHints', (props) => {
|
|
|
174
182
|
{ keys: 'esc', label: 'read' },
|
|
175
183
|
] }));
|
|
176
184
|
}
|
|
185
|
+
if (inPanel) {
|
|
186
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
187
|
+
{ keys: upDown, label: 'move' },
|
|
188
|
+
{ keys: 'enter', label: 'choose' },
|
|
189
|
+
{ keys: 'esc', label: 'back' },
|
|
190
|
+
{ keys: 'ctrl+c', label: running ? 'stop' : 'quit' },
|
|
191
|
+
] }));
|
|
192
|
+
}
|
|
193
|
+
if ((screen === 'chat' || screen === 'new') && (onChip || onSend)) {
|
|
194
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
195
|
+
{ keys: 'enter', label: onSend ? (screen === 'new' ? 'start' : running ? 'queue' : 'send') : 'open' },
|
|
196
|
+
{ keys: 'tab', label: 'next option' },
|
|
197
|
+
{ keys: 'esc', label: 'write' },
|
|
198
|
+
{ keys: 'ctrl+c', label: running ? 'stop' : 'quit' },
|
|
199
|
+
] }));
|
|
200
|
+
}
|
|
177
201
|
if (screen === 'chat') {
|
|
178
202
|
return (_jsx(KeyHints, { ...props, hints: composing
|
|
179
203
|
? [
|
|
@@ -293,6 +317,9 @@ export function registerChat(app, options = {}) {
|
|
|
293
317
|
app.store.set(BOOD_FLOAT, options.boodFloat ?? false);
|
|
294
318
|
bag.add(controller);
|
|
295
319
|
bag.add(app.services.provide(CONTROLLER, controller));
|
|
320
|
+
// `file:` is the host's disk from here on, for the picker and anything
|
|
321
|
+
// else in textui that reads the resource registry.
|
|
322
|
+
bag.add(app.resources.registerProvider(hostResources(host)));
|
|
296
323
|
for (const [component, render] of [
|
|
297
324
|
['ChatBubble', ChatBubble],
|
|
298
325
|
['StreamingText', StreamingText],
|
package/dist/src/blocks.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Block } from '@textui/chat';
|
|
2
|
+
import type { QueuedMessage, Turn } from './ahp/types.js';
|
|
3
|
+
export type { Block } from '@textui/chat';
|
|
4
|
+
export { selectable } from '@textui/chat';
|
|
2
5
|
/**
|
|
3
6
|
* A conversation, flattened into the rows a viewport scrolls.
|
|
4
7
|
*
|
|
@@ -12,77 +15,4 @@ import type { QueuedMessage, ToolCall, Turn } from './ahp/types.js';
|
|
|
12
15
|
* in one stream, and "let me search for those" means something before the
|
|
13
16
|
* searches and nothing after them.
|
|
14
17
|
*/
|
|
15
|
-
export type Block = {
|
|
16
|
-
kind: 'said';
|
|
17
|
-
id: string;
|
|
18
|
-
turnId: string;
|
|
19
|
-
text: string;
|
|
20
|
-
} | {
|
|
21
|
-
kind: 'header';
|
|
22
|
-
id: string;
|
|
23
|
-
turnId: string;
|
|
24
|
-
model?: string;
|
|
25
|
-
/**
|
|
26
|
-
* What the turn was asked for besides the model, in the host's words.
|
|
27
|
-
*
|
|
28
|
-
* The values rather than the keys: `thinkingLevel` is one host's name for
|
|
29
|
-
* a property whose *answers* are what a person reads, and a header that
|
|
30
|
-
* spelled out the key would be twice as long and no clearer.
|
|
31
|
-
*/
|
|
32
|
-
settings?: string;
|
|
33
|
-
meta: string;
|
|
34
|
-
state: Turn['state'];
|
|
35
|
-
} | {
|
|
36
|
-
kind: 'prose';
|
|
37
|
-
id: string;
|
|
38
|
-
turnId: string;
|
|
39
|
-
content: string;
|
|
40
|
-
streaming: boolean;
|
|
41
|
-
} | {
|
|
42
|
-
kind: 'reasoning';
|
|
43
|
-
id: string;
|
|
44
|
-
turnId: string;
|
|
45
|
-
content: string;
|
|
46
|
-
streaming: boolean;
|
|
47
|
-
} | {
|
|
48
|
-
kind: 'notice';
|
|
49
|
-
id: string;
|
|
50
|
-
turnId: string;
|
|
51
|
-
content: string;
|
|
52
|
-
} | {
|
|
53
|
-
kind: 'failure';
|
|
54
|
-
id: string;
|
|
55
|
-
turnId: string;
|
|
56
|
-
content: string;
|
|
57
|
-
resumable: boolean;
|
|
58
|
-
} | {
|
|
59
|
-
kind: 'tool';
|
|
60
|
-
id: string;
|
|
61
|
-
turnId: string;
|
|
62
|
-
call: ToolCall;
|
|
63
|
-
} | {
|
|
64
|
-
kind: 'queued';
|
|
65
|
-
id: string;
|
|
66
|
-
messageId: string;
|
|
67
|
-
text: string;
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* Everything in a block that a person could be looking for.
|
|
71
|
-
*
|
|
72
|
-
* A tool call is its name, its command and what came back, because all three
|
|
73
|
-
* are things somebody searches a transcript for - the file a command touched
|
|
74
|
-
* is in the output and nowhere else. A header is the model and the settings,
|
|
75
|
-
* which is how "where did I switch to opus" is answered.
|
|
76
|
-
*/
|
|
77
|
-
export declare function blockText(block: Block): string;
|
|
78
|
-
/**
|
|
79
|
-
* Where in the conversation a query appears, as block indices in order.
|
|
80
|
-
*
|
|
81
|
-
* Case-insensitive, and a blank query matches nothing rather than everything:
|
|
82
|
-
* a find with no term is a find that has not been typed yet, and lighting up
|
|
83
|
-
* every block for it is the opposite of what the box is for.
|
|
84
|
-
*/
|
|
85
|
-
export declare function findBlocks(blocks: Block[], query: string): number[];
|
|
86
|
-
/** Blocks the cursor stops on: the ones that do something when activated. */
|
|
87
|
-
export declare function selectable(block: Block): boolean;
|
|
88
18
|
export declare function toBlocks(turns: Turn[], queued?: QueuedMessage[]): Block[];
|
package/dist/src/blocks.js
CHANGED
|
@@ -1,55 +1,17 @@
|
|
|
1
|
+
export { selectable } from '@textui/chat';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* A conversation, flattened into the rows a viewport scrolls.
|
|
3
4
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
export function blockText(block) {
|
|
10
|
-
switch (block.kind) {
|
|
11
|
-
case 'said':
|
|
12
|
-
case 'queued':
|
|
13
|
-
return block.text;
|
|
14
|
-
case 'prose':
|
|
15
|
-
case 'reasoning':
|
|
16
|
-
case 'notice':
|
|
17
|
-
case 'failure':
|
|
18
|
-
return block.content;
|
|
19
|
-
case 'header':
|
|
20
|
-
return [block.model, block.settings, block.meta].filter(Boolean).join(' ');
|
|
21
|
-
case 'tool':
|
|
22
|
-
return [
|
|
23
|
-
block.call.name, block.call.toolName, block.call.input,
|
|
24
|
-
block.call.intention, block.call.outcome, block.call.output,
|
|
25
|
-
...(block.call.files ?? []),
|
|
26
|
-
].filter(Boolean).join(' ');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Where in the conversation a query appears, as block indices in order.
|
|
5
|
+
* A turn is not a box. It is a run of rows - a header, some prose, a tool
|
|
6
|
+
* call, more prose - and the transcript has to be able to put its cursor on
|
|
7
|
+
* one of them, measure it, and scroll to it. Nesting each turn inside a
|
|
8
|
+
* container would mean the transcript could only ever scroll to a whole turn,
|
|
9
|
+
* and a turn can be four hundred rows long.
|
|
31
10
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
11
|
+
* The order is the host's order. `responseParts` interleaves prose and calls
|
|
12
|
+
* in one stream, and "let me search for those" means something before the
|
|
13
|
+
* searches and nothing after them.
|
|
35
14
|
*/
|
|
36
|
-
export function findBlocks(blocks, query) {
|
|
37
|
-
const needle = query.trim().toLowerCase();
|
|
38
|
-
if (needle === '')
|
|
39
|
-
return [];
|
|
40
|
-
const found = [];
|
|
41
|
-
blocks.forEach((block, index) => {
|
|
42
|
-
if (blockText(block).toLowerCase().includes(needle))
|
|
43
|
-
found.push(index);
|
|
44
|
-
});
|
|
45
|
-
return found;
|
|
46
|
-
}
|
|
47
|
-
/** Blocks the cursor stops on: the ones that do something when activated. */
|
|
48
|
-
export function selectable(block) {
|
|
49
|
-
// A queued message among them, because taking one back is something you do
|
|
50
|
-
// to it - and the cursor is how anything in the transcript is reached.
|
|
51
|
-
return block.kind === 'tool' || block.kind === 'reasoning' || block.kind === 'queued';
|
|
52
|
-
}
|
|
53
15
|
export function toBlocks(turns, queued = []) {
|
|
54
16
|
const blocks = [];
|
|
55
17
|
for (const turn of turns) {
|
package/dist/src/cli/main.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Every command, and the argv reading that picks one. */
|
|
2
|
-
export declare const HELP = "ahpc - drive an agent host from a shell\n\n ahpc [--host ws://\u2026] <command> [args] the screen is 'ahpc' with no command\n\nSessions\n session list the catalogue, newest first [--archived] [--json]\n session show <uri> what the host says about one [--full] [--json]\n session new start one [--agent P] [--cwd DIR] [--set k=v]\u2026 [--json]\n session rm <uri> dispose it\n session history <uri> its turns [--all] [--full] [--json]\n session config <uri> the schema and what is in force [--json]\n session set <uri> <k> <v> change one config key\n session read <uri> mark read [--unread]\n session archive <uri> put it away [--undo]\n session customizations <uri> skills, prompts, agents, servers [--json]\n session export <uri> the whole session as one document\n [--json] [--markdown]\n session toggle <uri> <id> turn one on [--off]\n\nTurns\n prompt <uri> <text> say it and stream the answer [--model M] [--json]\n exec <text> a session, one turn, and dispose it\n [--agent P] [--cwd DIR] [--model M] [--json]\n cancel <uri> stop the running turn\n queue <uri> <text> say it after the one running [--model M]\n unqueue <uri> <id> take it back\n\nAnswering\n watch <uri> BLOCK until something wants a person, print, exit\n [--until turn|input|idle] [--timeout S] [--json]\n confirm <uri> <toolCallId> approve a tool call [--deny] [--option ID]\n answer <uri> <requestId> answer a question [--field k=v]\u2026 [--reject]\n\nChats\n chat list <uri> the conversations in a session [--json]\n chat new <uri> [text] another one beside it\n chat rm <chatUri> close one\n\nThe harness\n agents what it serves, and each one's models [--json]\n models every model, by harness [--json]\n commands what a slash offers [--json]\n customizations skills, prompts, agents and MCP servers,\n before any session exists [--kind k] [--json]\n completions <uri> <text> what the host would complete [--offset N] [--json]\n\nChanges and files\n changes <uri> the files a session touched [--json]\n [--list] [--scope s] [--<variable> v]\n [--reviewed f] [--unreviewed f]\n [--operations] what may be done to it\n [--run id] [--file f] [--yes] do one of them\n [--list] every changeset it offers\n [--scope <name>] one of them, e.g. turn\n [--turnId <id>] what a chosen scope still needs\n [--reviewed <file>] tick one off, repeatable\n [--unreviewed <file>] and clear one\n content <uri> <file> one of them, in full\n resource list <uri> a directory the host serves [--json]\n resource read <uri> a file on the host\n resource stat <uri> what it is, without reading it [--json]\n resource write <uri> [file] from a file, or from stdin [--create-only]\n guarded by the file's etag unless [--force]\n resource rm <uri> delete it [--recursive]\n resource mkdir <uri> make a directory\n resource mv <uri> <to> move it [--fail-if-exists]\n resource cp <uri> <to> copy it [--fail-if-exists]\n\nAutomations\n automation list what runs on its own [--json]\n automation show <uri> one of them [--json]\n automation triggers what this host can trigger on [--json]\n automation runs <uri> its history, every page [--json]\n automation run <uri> start it now\n automation enable <uri> switch it on\n automation disable <uri> switch it off\n automation rm <uri> forget it\n\nThe host's own log\n logs what the daemon is saying [--level L] [--follow]\n\nSigning in\n auth what this host protects [--json]\n auth <resource> push a token [--token T] [--expires-in S]\n or set AHPC_TOKEN_<RESOURCE>, or pipe one in\n\nTerminals\n terminal list what is running [--json]\n terminal new open a shell [--cwd DIR] [--name N]\n terminal rm <uri> kill it\n terminal send <uri> <text> type into it\n terminal watch <uri> follow its output [--timeout S]\n\nRecording\n
|
|
2
|
+
export declare const HELP = "ahpc - drive an agent host from a shell\n\n ahpc [--host ws://\u2026] <command> [args] the screen is 'ahpc' with no command\n\nSessions\n session list the catalogue, newest first [--archived] [--json]\n session show <uri> what the host says about one [--full] [--json]\n session new start one [--agent P] [--cwd DIR] [--set k=v]\u2026 [--json]\n session rm <uri> dispose it\n session history <uri> its turns [--all] [--full] [--json]\n session config <uri> the schema and what is in force [--json]\n session set <uri> <k> <v> change one config key\n session read <uri> mark read [--unread]\n session archive <uri> put it away [--undo]\n session customizations <uri> skills, prompts, agents, servers [--json]\n session export <uri> the whole session as one document\n [--json] [--markdown]\n session toggle <uri> <id> turn one on [--off]\n\nTurns\n prompt <uri> <text> say it and stream the answer [--model M] [--json]\n exec <text> a session, one turn, and dispose it\n [--agent P] [--cwd DIR] [--model M] [--json]\n cancel <uri> stop the running turn\n queue <uri> <text> say it after the one running [--model M]\n unqueue <uri> <id> take it back\n\nAnswering\n watch <uri> BLOCK until something wants a person, print, exit\n [--until turn|input|idle] [--timeout S] [--json]\n confirm <uri> <toolCallId> approve a tool call [--deny] [--option ID]\n answer <uri> <requestId> answer a question [--field k=v]\u2026 [--reject]\n\nChats\n chat list <uri> the conversations in a session [--json]\n chat new <uri> [text] another one beside it\n chat rm <chatUri> close one\n\nThe harness\n agents what it serves, and each one's models [--json]\n models every model, by harness [--json]\n commands what a slash offers [--json]\n customizations skills, prompts, agents and MCP servers,\n before any session exists [--kind k] [--json]\n completions <uri> <text> what the host would complete [--offset N] [--json]\n\nChanges and files\n changes <uri> the files a session touched [--json]\n [--list] [--scope s] [--<variable> v]\n [--reviewed f] [--unreviewed f]\n [--operations] what may be done to it\n [--run id] [--file f] [--yes] do one of them\n [--list] every changeset it offers\n [--scope <name>] one of them, e.g. turn\n [--turnId <id>] what a chosen scope still needs\n [--reviewed <file>] tick one off, repeatable\n [--unreviewed <file>] and clear one\n content <uri> <file> one of them, in full\n resource list <uri> a directory the host serves [--json]\n resource read <uri> a file on the host\n resource stat <uri> what it is, without reading it [--json]\n resource write <uri> [file] from a file, or from stdin [--create-only]\n guarded by the file's etag unless [--force]\n resource rm <uri> delete it [--recursive]\n resource mkdir <uri> make a directory\n resource mv <uri> <to> move it [--fail-if-exists]\n resource cp <uri> <to> copy it [--fail-if-exists]\n\nAutomations\n automation list what runs on its own [--json]\n automation show <uri> one of them [--json]\n automation triggers what this host can trigger on [--json]\n automation runs <uri> its history, every page [--json]\n automation run <uri> start it now\n automation enable <uri> switch it on\n automation disable <uri> switch it off\n automation rm <uri> forget it\n\nThe host's own log\n logs what the daemon is saying [--level L] [--follow]\n\nSigning in\n auth what this host protects [--json]\n auth <resource> push a token [--token T] [--expires-in S]\n or set AHPC_TOKEN_<RESOURCE>, or pipe one in\n\nTerminals\n terminal list what is running [--json]\n terminal new open a shell [--cwd DIR] [--name N]\n terminal rm <uri> kill it\n terminal send <uri> <text> type into it\n terminal watch <uri> follow its output [--timeout S]\n\nRecording\n --wire <file> append every frame, both directions, as JSON\n lines: { at, from, peer, frame }, for\n 'npm run wire' to check against the protocol.\n AHPC_RECORD=<file> is the same, from a shell\n wire <file> watch a capture as it is written, from either\n end: one row per frame, a row opens to the\n frame. Off a terminal, one line per frame\n [--follow] [--filter text] [--json]\n\nServing these sessions to something else\n mcp MCP on stdin and stdout, for a client that\n launches this process\n serve the same tools on a socket, shared\n [--serve-host H] [--serve-port N] [--serve-token T]\n [--serve-origin URL]\u2026 a browser page allowed in\n /mcp is MCP; /api/<tool> is plain JSON\n both take [--mcp-tools G,\u2026] extra tool groups to serve, on top of the\n sessions ones: resources, terminals,\n automations, changes\n\nAnything else\n dispatch <uri> <type> send one action verbatim [--field k=v]\u2026 [--chat]\n <uri>, anywhere above a session URI, or the agent-host-session://\n link the host's session tools answer with\n status what this client is connected to [--json]\n --version what version this is\n help this\n\nThe host\n --host <url> ws://host:port, or AHPC_HOST, or the config file\n --token <tkn> a bearer token for it, or AHPC_TOKEN, or the config file\n --config-file read this instead of the one below\n (none) the scripted host, which needs nothing installed\n\nConfiguration\n config where the file is, and what is in force [--json]\n\nOutput is for reading. --json is the same answer for a program.\n";
|
|
3
3
|
/** A message for the person, not a stack trace. */
|
|
4
4
|
export declare class Fault extends Error {
|
|
5
5
|
}
|
package/dist/src/cli/main.js
CHANGED
|
@@ -5,8 +5,9 @@ import { configPath, loadConfig } from '../config.js';
|
|
|
5
5
|
import { ago, archived, branch, json, line, mark, project, table } from './render.js';
|
|
6
6
|
import { operate } from '../ahp/operate.js';
|
|
7
7
|
import { SWITCHES } from '../flags.js';
|
|
8
|
+
import { parseSessionLink, sessionOfLink } from '../links.js';
|
|
8
9
|
import { spoken, turn as runTurn, until } from '../wait.js';
|
|
9
|
-
import {
|
|
10
|
+
import { GROUPS, served } from '../mcp/tools.js';
|
|
10
11
|
import { SERVER } from '../mcp/serve.js';
|
|
11
12
|
import { stdio } from '../mcp/stdio.js';
|
|
12
13
|
import { serve as serveHttp } from '../mcp/http.js';
|
|
@@ -104,19 +105,32 @@ Terminals
|
|
|
104
105
|
terminal watch <uri> follow its output [--timeout S]
|
|
105
106
|
|
|
106
107
|
Recording
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
--wire <file> append every frame, both directions, as JSON
|
|
109
|
+
lines: { at, from, peer, frame }, for
|
|
110
|
+
'npm run wire' to check against the protocol.
|
|
111
|
+
AHPC_RECORD=<file> is the same, from a shell
|
|
112
|
+
wire <file> watch a capture as it is written, from either
|
|
113
|
+
end: one row per frame, a row opens to the
|
|
114
|
+
frame. Off a terminal, one line per frame
|
|
115
|
+
[--follow] [--filter text] [--json]
|
|
109
116
|
|
|
110
117
|
Serving these sessions to something else
|
|
111
118
|
mcp MCP on stdin and stdout, for a client that
|
|
112
119
|
launches this process
|
|
113
120
|
serve the same tools on a socket, shared
|
|
114
121
|
[--serve-host H] [--serve-port N] [--serve-token T]
|
|
122
|
+
[--serve-origin URL]… a browser page allowed in
|
|
115
123
|
/mcp is MCP; /api/<tool> is plain JSON
|
|
124
|
+
both take [--mcp-tools G,…] extra tool groups to serve, on top of the
|
|
125
|
+
sessions ones: resources, terminals,
|
|
126
|
+
automations, changes
|
|
116
127
|
|
|
117
128
|
Anything else
|
|
118
129
|
dispatch <uri> <type> send one action verbatim [--field k=v]… [--chat]
|
|
130
|
+
<uri>, anywhere above a session URI, or the agent-host-session://
|
|
131
|
+
link the host's session tools answer with
|
|
119
132
|
status what this client is connected to [--json]
|
|
133
|
+
--version what version this is
|
|
120
134
|
help this
|
|
121
135
|
|
|
122
136
|
The host
|
|
@@ -142,6 +156,24 @@ class Args {
|
|
|
142
156
|
constructor(rest) {
|
|
143
157
|
this.rest = rest;
|
|
144
158
|
}
|
|
159
|
+
/** Every positional that is a link, replaced by what it names. */
|
|
160
|
+
async resolveLinks(resolve) {
|
|
161
|
+
const next = [...this.rest];
|
|
162
|
+
for (let i = 0; i < next.length; i++) {
|
|
163
|
+
const word = next[i];
|
|
164
|
+
if (word.startsWith('--')) {
|
|
165
|
+
if (!SWITCHES.has(word))
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!/^agent-host-session:\/\//i.test(word))
|
|
170
|
+
continue;
|
|
171
|
+
const found = await resolve(word);
|
|
172
|
+
if (found !== undefined)
|
|
173
|
+
next[i] = found;
|
|
174
|
+
}
|
|
175
|
+
this.rest = next;
|
|
176
|
+
}
|
|
145
177
|
/** The nth thing that is not a flag or a flag's value. */
|
|
146
178
|
positional(index) {
|
|
147
179
|
const found = [];
|
|
@@ -204,6 +236,7 @@ const where = (args) => {
|
|
|
204
236
|
// write into it. Both off unless asked for.
|
|
205
237
|
...(args.value('--publish') ? { publish: args.value('--publish') } : {}),
|
|
206
238
|
...(args.has('--publish-writable') ? { publishWritable: true } : {}),
|
|
239
|
+
...(args.value('--wire') ? { wire: args.value('--wire') } : {}),
|
|
207
240
|
};
|
|
208
241
|
};
|
|
209
242
|
/** A URI the command needs, said plainly when it is missing. */
|
|
@@ -309,8 +342,53 @@ export async function cli(command, rest) {
|
|
|
309
342
|
table(rows);
|
|
310
343
|
return 0;
|
|
311
344
|
}
|
|
345
|
+
/*
|
|
346
|
+
* A capture is a file, not a host.
|
|
347
|
+
*
|
|
348
|
+
* `ahpc wire out.jsonl` reads what `--wire` wrote - here or on the host -
|
|
349
|
+
* and needs no connection to do it; needing one would make it useless on
|
|
350
|
+
* exactly the capture of a host that would not answer.
|
|
351
|
+
*/
|
|
352
|
+
if (command === 'wire') {
|
|
353
|
+
const file = args.positional(0);
|
|
354
|
+
if (file === undefined)
|
|
355
|
+
throw new Fault('wire wants a file: ahpc wire <file>');
|
|
356
|
+
const settings = loadConfig('ahpc', args.value('--config-file'));
|
|
357
|
+
const { wireTui } = await import('../wiretui.js');
|
|
358
|
+
const theme = args.value('--theme') ?? settings.theme;
|
|
359
|
+
const shell = args.value('--shell') ?? settings.shell;
|
|
360
|
+
const filter = args.value('--filter');
|
|
361
|
+
await wireTui({
|
|
362
|
+
file,
|
|
363
|
+
follow: args.has('--follow'),
|
|
364
|
+
json: wants,
|
|
365
|
+
...(filter === undefined ? {} : { filter }),
|
|
366
|
+
...(theme === undefined ? {} : { theme }),
|
|
367
|
+
...(shell === undefined ? {} : { shell }),
|
|
368
|
+
});
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
312
371
|
const host = await connect(where(args));
|
|
313
372
|
try {
|
|
373
|
+
/*
|
|
374
|
+
* A link where a URI is wanted.
|
|
375
|
+
*
|
|
376
|
+
* The reference host's session tools answer with `agent-host-session://`
|
|
377
|
+
* links, and a person pasting one into `session show` should get the
|
|
378
|
+
* session it names rather than a refusal about the scheme. Resolved once,
|
|
379
|
+
* against the catalogue, before any command reads its arguments; a link
|
|
380
|
+
* nobody on this host answers to is a sentence rather than a lookup that
|
|
381
|
+
* silently found nothing.
|
|
382
|
+
*/
|
|
383
|
+
await args.resolveLinks(async (link) => {
|
|
384
|
+
const parsed = parseSessionLink(link);
|
|
385
|
+
if (parsed === undefined)
|
|
386
|
+
return undefined;
|
|
387
|
+
const row = sessionOfLink(parsed, await host.listSessions());
|
|
388
|
+
if (row === undefined)
|
|
389
|
+
throw new Fault(`No session on this host matches ${link}.`);
|
|
390
|
+
return row.resource;
|
|
391
|
+
});
|
|
314
392
|
switch (command) {
|
|
315
393
|
case 'status': {
|
|
316
394
|
const rows = await host.listSessions().catch(() => []);
|
|
@@ -331,22 +409,27 @@ export async function cli(command, rest) {
|
|
|
331
409
|
* `/api/<tool>` for anything that is not an MCP client.
|
|
332
410
|
*/
|
|
333
411
|
case 'mcp': {
|
|
412
|
+
const groups = wanted(args);
|
|
334
413
|
// Nothing but JSON-RPC on stdout, ever: a stray line here is a parse
|
|
335
414
|
// error at the other end of a pipe nobody can see.
|
|
336
|
-
process.stderr.write(`ahpc mcp on ${host.url || '(scripted host)'}, ${
|
|
337
|
-
await stdio(host, { ...SERVER, onProblem: (said) => process.stderr.write(`${said}\n`) });
|
|
415
|
+
process.stderr.write(`ahpc mcp on ${host.url || '(scripted host)'}, ${served(groups).length} tools\n`);
|
|
416
|
+
await stdio(host, { ...SERVER, groups, onProblem: (said) => process.stderr.write(`${said}\n`) });
|
|
338
417
|
return 0;
|
|
339
418
|
}
|
|
340
419
|
case 'serve': {
|
|
420
|
+
const groups = wanted(args);
|
|
341
421
|
const at = await serveHttp(host, {
|
|
342
422
|
...SERVER,
|
|
423
|
+
groups,
|
|
343
424
|
host: args.value('--serve-host') ?? '127.0.0.1',
|
|
344
425
|
port: Number(args.value('--serve-port') ?? 7431),
|
|
345
426
|
...(args.value('--serve-token') === undefined ? {} : { token: args.value('--serve-token') }),
|
|
427
|
+
// Repeatable, because a page and its API are often two origins.
|
|
428
|
+
origins: args.every('--serve-origin'),
|
|
346
429
|
onProblem: (said) => process.stderr.write(`${said}\n`),
|
|
347
430
|
});
|
|
348
431
|
line(`ahpc on http://${at.host}:${at.port} against ${host.url || '(scripted host)'}`);
|
|
349
|
-
line(` /mcp MCP, ${
|
|
432
|
+
line(` /mcp MCP, ${served(groups).length} tools`);
|
|
350
433
|
line(' /api/<tool> the same tools as plain JSON');
|
|
351
434
|
if (args.value('--serve-token') === undefined && at.host !== '127.0.0.1' && at.host !== '::1') {
|
|
352
435
|
// Said rather than refused: binding wide open is a decision somebody
|
|
@@ -997,6 +1080,26 @@ async function chats(host, args, wants) {
|
|
|
997
1080
|
}
|
|
998
1081
|
throw new Fault(`No 'chat ${verb}'. Try 'ahpc help'.`);
|
|
999
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* The tool groups `--mcp-tools` asked for.
|
|
1085
|
+
*
|
|
1086
|
+
* Comma-separated and repeatable, because both are what people type. Named
|
|
1087
|
+
* `--mcp-tools` and not `--tools`: every other flag on this client is an AHP
|
|
1088
|
+
* thing, and a bare `--tools` reads like one - it would look like it was
|
|
1089
|
+
* choosing which tools the *agent* may call, which is a different question
|
|
1090
|
+
* with a different answer.
|
|
1091
|
+
*/
|
|
1092
|
+
function wanted(args) {
|
|
1093
|
+
const said = args.every('--mcp-tools')
|
|
1094
|
+
.flatMap((one) => one.split(','))
|
|
1095
|
+
.map((one) => one.trim())
|
|
1096
|
+
.filter((one) => one !== '');
|
|
1097
|
+
const strange = said.filter((one) => !GROUPS.includes(one));
|
|
1098
|
+
if (strange.length > 0) {
|
|
1099
|
+
throw new Fault(`No tool group called ${strange.join(', ')}. There is ${GROUPS.join(', ')}.`);
|
|
1100
|
+
}
|
|
1101
|
+
return said;
|
|
1102
|
+
}
|
|
1000
1103
|
/** Everything under `terminal`. */
|
|
1001
1104
|
async function shells(host, args, wants) {
|
|
1002
1105
|
const verb = args.positional(0) ?? 'list';
|
package/dist/src/connect.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface Where {
|
|
|
29
29
|
publish?: string;
|
|
30
30
|
/** Whether the published directory may be written to. Read-only otherwise. */
|
|
31
31
|
publishWritable?: boolean;
|
|
32
|
+
/** A file every frame is appended to, both directions, as JSON lines. */
|
|
33
|
+
wire?: string;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Where a refusal goes before there is an application to put it in.
|
package/dist/src/connect.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function connect(options) {
|
|
|
27
27
|
return await liveHost({
|
|
28
28
|
url: options.host,
|
|
29
29
|
...(options.token ? { token: options.token } : {}),
|
|
30
|
+
...(options.wire ? { wire: options.wire } : {}),
|
|
30
31
|
onRefusal: (_uri, message) => sink.report(message),
|
|
31
32
|
onLimit: (message) => sink.report(message),
|
|
32
33
|
// Work the host is doing under a token of its own. Reported while it
|
package/dist/src/control.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ import type { Agent, Answer, Automation, Changeset, ChangesetScope, ChatSource,
|
|
|
17
17
|
export interface Controller {
|
|
18
18
|
refresh(): Promise<void>;
|
|
19
19
|
open(uri: SessionUri): void;
|
|
20
|
+
/**
|
|
21
|
+
* Open what an `agent-host-session://` link names: the session, and the
|
|
22
|
+
* chat in it when the link says one. False when this host has no such
|
|
23
|
+
* session, said out loud as well.
|
|
24
|
+
*/
|
|
25
|
+
openLink(link: string): Promise<boolean>;
|
|
20
26
|
close(): void;
|
|
21
27
|
/** Send, or queue when a turn is already running. */
|
|
22
28
|
send(text: string): void;
|