@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.
Files changed (73) hide show
  1. package/README.md +37 -6
  2. package/dist/src/ahp/fake.js +61 -14
  3. package/dist/src/ahp/live.d.ts +7 -0
  4. package/dist/src/ahp/live.js +156 -43
  5. package/dist/src/ahp/publish.js +13 -0
  6. package/dist/src/ahp/types.d.ts +20 -0
  7. package/dist/src/app.js +34 -7
  8. package/dist/src/blocks.d.ts +4 -74
  9. package/dist/src/blocks.js +10 -48
  10. package/dist/src/cli/main.d.ts +1 -1
  11. package/dist/src/cli/main.js +109 -6
  12. package/dist/src/connect.d.ts +2 -0
  13. package/dist/src/connect.js +1 -0
  14. package/dist/src/control.d.ts +6 -0
  15. package/dist/src/control.js +140 -10
  16. package/dist/src/flags.js +4 -1
  17. package/dist/src/links.d.ts +54 -0
  18. package/dist/src/links.js +120 -0
  19. package/dist/src/main.js +14 -2
  20. package/dist/src/mcp/http.d.ts +10 -0
  21. package/dist/src/mcp/http.js +110 -3
  22. package/dist/src/mcp/serve.d.ts +47 -9
  23. package/dist/src/mcp/serve.js +65 -14
  24. package/dist/src/mcp/stdio.d.ts +1 -0
  25. package/dist/src/mcp/stdio.js +7 -1
  26. package/dist/src/mcp/tools.d.ts +37 -2
  27. package/dist/src/mcp/tools.js +439 -2
  28. package/dist/src/resources.d.ts +13 -0
  29. package/dist/src/resources.js +46 -0
  30. package/dist/src/screens.js +95 -50
  31. package/dist/src/state.d.ts +42 -0
  32. package/dist/src/state.js +80 -1
  33. package/dist/src/tui.d.ts +3 -1
  34. package/dist/src/tui.js +29 -4
  35. package/dist/src/version.d.ts +2 -0
  36. package/dist/src/version.js +38 -0
  37. package/dist/src/view/creature.d.ts +0 -12
  38. package/dist/src/view/creature.js +0 -20
  39. package/dist/src/view/wire.d.ts +36 -0
  40. package/dist/src/view/wire.js +196 -0
  41. package/dist/src/wait.d.ts +11 -0
  42. package/dist/src/wait.js +15 -2
  43. package/dist/src/wire.d.ts +70 -0
  44. package/dist/src/wire.js +194 -0
  45. package/dist/src/wiretui.d.ts +22 -0
  46. package/dist/src/wiretui.js +69 -0
  47. package/package.json +6 -5
  48. package/dist/src/diff.d.ts +0 -44
  49. package/dist/src/diff.js +0 -111
  50. package/dist/src/view/bubble.d.ts +0 -75
  51. package/dist/src/view/bubble.js +0 -86
  52. package/dist/src/view/composer.d.ts +0 -64
  53. package/dist/src/view/composer.js +0 -192
  54. package/dist/src/view/controls.d.ts +0 -44
  55. package/dist/src/view/controls.js +0 -49
  56. package/dist/src/view/details.d.ts +0 -65
  57. package/dist/src/view/details.js +0 -65
  58. package/dist/src/view/filediff.d.ts +0 -29
  59. package/dist/src/view/filediff.js +0 -24
  60. package/dist/src/view/hitl.d.ts +0 -43
  61. package/dist/src/view/hitl.js +0 -171
  62. package/dist/src/view/icons.d.ts +0 -13
  63. package/dist/src/view/icons.js +0 -71
  64. package/dist/src/view/picker.d.ts +0 -42
  65. package/dist/src/view/picker.js +0 -71
  66. package/dist/src/view/sessionhead.d.ts +0 -41
  67. package/dist/src/view/sessionhead.js +0 -60
  68. package/dist/src/view/sessions.d.ts +0 -34
  69. package/dist/src/view/sessions.js +0 -61
  70. package/dist/src/view/toolcall.d.ts +0 -27
  71. package/dist/src/view/toolcall.js +0 -48
  72. package/dist/src/view/transcript.d.ts +0 -50
  73. package/dist/src/view/transcript.js +0 -60
@@ -1,17 +1,26 @@
1
1
  import type { HostConnection } from '../ahp/connection.js';
2
- /** The version of MCP this speaks, and the one it answers `initialize` with. */
2
+ /** The newest version of MCP this speaks, and what it answers an unknown one with. */
3
3
  export declare const PROTOCOL = "2025-06-18";
4
+ /**
5
+ * Every version this will serve, newest first.
6
+ *
7
+ * Both of these describe the same tools, and this server uses nothing either
8
+ * of them added or removed - no sessions, no resources, no sampling - so
9
+ * refusing the older one would be refusing a client for a difference that
10
+ * cannot reach it. `2024-11-05` is *not* here: its HTTP transport is a
11
+ * different shape, with an `endpoint` event and a second URL.
12
+ */
13
+ export declare const SPOKEN: readonly string[];
4
14
  /**
5
15
  * What this server calls itself.
6
16
  *
7
- * MCP requires a version where AHP's `clientInfo` does not, so this is the
8
- * only version string in the source. Cosmetic - a client displays it and
9
- * nothing branches on it - and deliberately not read out of `package.json`,
10
- * which sits at a different depth in the published tree than it does here.
17
+ * MCP requires a version where AHP's `clientInfo` does not. Read from the
18
+ * manifest rather than written here: the literal that used to be here said
19
+ * 0.1 while the package said 0.2, within a day of being written.
11
20
  */
12
21
  export declare const SERVER: {
13
- readonly name: "ahpc";
14
- readonly version: "0.1";
22
+ name: string;
23
+ version: string;
15
24
  };
16
25
  /** A JSON-RPC request or notification, as far as this needs to read one. */
17
26
  export interface Incoming {
@@ -20,6 +29,21 @@ export interface Incoming {
20
29
  method?: unknown;
21
30
  params?: unknown;
22
31
  }
32
+ /**
33
+ * One progress notification, as `notifications/progress` puts it on the wire.
34
+ *
35
+ * `progress` MUST increase on every one, `total` is left out because an
36
+ * agent's reply has no length known in advance, and `message` is what a person
37
+ * reads - which is the tool name a session stopped on, or the host's own words
38
+ * for what it is doing.
39
+ */
40
+ export interface Progress {
41
+ progressToken: string | number;
42
+ progress: number;
43
+ message?: string;
44
+ }
45
+ /** Somewhere to put progress while a tool runs, or nothing where nobody asked. */
46
+ export type Report = ((said: string) => void) | undefined;
23
47
  /** What goes back, or nothing at all where the message was a notification. */
24
48
  export type Outgoing = {
25
49
  jsonrpc: '2.0';
@@ -35,7 +59,7 @@ export type Outgoing = {
35
59
  result?: never;
36
60
  });
37
61
  /** The tools, in the shape `tools/list` puts them on the wire. */
38
- export declare const listing: () => unknown;
62
+ export declare const listing: (groups?: readonly string[]) => unknown;
39
63
  /**
40
64
  * Run one tool and shape the answer the way `tools/call` wants it.
41
65
  *
@@ -45,7 +69,7 @@ export declare const listing: () => unknown;
45
69
  * fault it can only give up over. So a refusal from the host - a session that
46
70
  * is gone, a directory it does not serve - comes back as content.
47
71
  */
48
- export declare function call(host: HostConnection, name: string, input: unknown): Promise<unknown>;
72
+ export declare function call(host: HostConnection, name: string, input: unknown, report?: Report, groups?: readonly string[]): Promise<unknown>;
49
73
  /**
50
74
  * Answer one message.
51
75
  *
@@ -55,4 +79,18 @@ export declare function call(host: HostConnection, name: string, input: unknown)
55
79
  export declare function answer(host: HostConnection, message: Incoming, options: {
56
80
  name: string;
57
81
  version: string;
82
+ /** The opt-in tool groups this server was started with, if any. */
83
+ groups?: readonly string[];
84
+ /**
85
+ * Somewhere to send a notification while this is being answered.
86
+ *
87
+ * Given by a transport that has one: stdio always does, and HTTP does only
88
+ * once it has decided to answer with a stream. Absent means there is
89
+ * nowhere to say anything until the result, which is the ordinary case and
90
+ * why every progress path here is optional.
91
+ */
92
+ notify?(notification: {
93
+ method: string;
94
+ params: unknown;
95
+ }): void;
58
96
  }): Promise<Outgoing | undefined>;
@@ -13,25 +13,35 @@
13
13
  * obliged to answer them, and a caller that asks is told the method is not
14
14
  * there, which is the truth.
15
15
  */
16
- import { TOOLS, named } from './tools.js';
17
- /** The version of MCP this speaks, and the one it answers `initialize` with. */
16
+ import { named, served } from './tools.js';
17
+ import { version } from '../version.js';
18
+ /** The newest version of MCP this speaks, and what it answers an unknown one with. */
18
19
  export const PROTOCOL = '2025-06-18';
20
+ /**
21
+ * Every version this will serve, newest first.
22
+ *
23
+ * Both of these describe the same tools, and this server uses nothing either
24
+ * of them added or removed - no sessions, no resources, no sampling - so
25
+ * refusing the older one would be refusing a client for a difference that
26
+ * cannot reach it. `2024-11-05` is *not* here: its HTTP transport is a
27
+ * different shape, with an `endpoint` event and a second URL.
28
+ */
29
+ export const SPOKEN = ['2025-06-18', '2025-03-26'];
19
30
  /**
20
31
  * What this server calls itself.
21
32
  *
22
- * MCP requires a version where AHP's `clientInfo` does not, so this is the
23
- * only version string in the source. Cosmetic - a client displays it and
24
- * nothing branches on it - and deliberately not read out of `package.json`,
25
- * which sits at a different depth in the published tree than it does here.
33
+ * MCP requires a version where AHP's `clientInfo` does not. Read from the
34
+ * manifest rather than written here: the literal that used to be here said
35
+ * 0.1 while the package said 0.2, within a day of being written.
26
36
  */
27
- export const SERVER = { name: 'ahpc', version: '0.1' };
37
+ export const SERVER = { name: 'ahpc', version: version() };
28
38
  const METHOD_NOT_FOUND = -32601;
29
39
  const INVALID_PARAMS = -32602;
30
40
  const INTERNAL = -32603;
31
41
  const bag = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {});
32
42
  /** The tools, in the shape `tools/list` puts them on the wire. */
33
- export const listing = () => ({
34
- tools: TOOLS.map((tool) => ({
43
+ export const listing = (groups = []) => ({
44
+ tools: served(groups).map((tool) => ({
35
45
  name: tool.name,
36
46
  title: tool.title,
37
47
  description: tool.description,
@@ -48,7 +58,7 @@ export const listing = () => ({
48
58
  * fault it can only give up over. So a refusal from the host - a session that
49
59
  * is gone, a directory it does not serve - comes back as content.
50
60
  */
51
- export async function call(host, name, input) {
61
+ export async function call(host, name, input, report, groups = []) {
52
62
  const tool = named(name);
53
63
  if (tool === undefined) {
54
64
  return {
@@ -56,8 +66,17 @@ export async function call(host, name, input) {
56
66
  content: [{ type: 'text', text: `No tool called ${name}. Ask tools/list for what there is.` }],
57
67
  };
58
68
  }
69
+ // Exists, but this server was not started with it. Said as itself rather
70
+ // than as "no such tool", because the difference is one flag and only the
71
+ // person who started this can supply it.
72
+ if (tool.group !== undefined && !groups.includes(tool.group)) {
73
+ return {
74
+ isError: true,
75
+ content: [{ type: 'text', text: `${name} is in the ${tool.group} group, which this server was not started with. It needs --mcp-tools ${tool.group}.` }],
76
+ };
77
+ }
59
78
  try {
60
- const answer = await tool.run(host, bag(input));
79
+ const answer = await tool.run(host, bag(input), report);
61
80
  return {
62
81
  // Both, because clients differ: the text is what a model reads and
63
82
  // `structuredContent` is what a program does, and sending only the
@@ -75,6 +94,28 @@ export async function call(host, name, input) {
75
94
  };
76
95
  }
77
96
  }
97
+ /**
98
+ * Somewhere for a running tool to say what it is doing, if anybody asked.
99
+ *
100
+ * A caller opts in by putting a `progressToken` in `_meta` on the request, and
101
+ * `undefined` here means it did not - in which case a tool that reports
102
+ * anything is writing to nowhere, which is exactly what should happen. The
103
+ * counter is this closure's, because `progress` MUST increase and a tool
104
+ * counting for itself would be a tool that has to know about the protocol.
105
+ */
106
+ function reporter(message, notify) {
107
+ const token = bag(bag(message.params)._meta).progressToken;
108
+ if (notify === undefined)
109
+ return undefined;
110
+ if (typeof token !== 'string' && typeof token !== 'number')
111
+ return undefined;
112
+ let count = 0;
113
+ return (said) => {
114
+ count += 1;
115
+ const params = { progressToken: token, progress: count, message: said };
116
+ notify({ method: 'notifications/progress', params });
117
+ };
118
+ }
78
119
  /**
79
120
  * Answer one message.
80
121
  *
@@ -92,8 +133,18 @@ export async function answer(host, message, options) {
92
133
  const ok = (result) => ({ jsonrpc: '2.0', id, result });
93
134
  const no = (code, said) => ({ jsonrpc: '2.0', id, error: { code, message: said } });
94
135
  if (method === 'initialize') {
136
+ /*
137
+ * The client's version where this can speak it, and the newest otherwise.
138
+ *
139
+ * `lifecycle` says a server answering a version it was not asked for is
140
+ * telling the client to take that one or disconnect, so answering our
141
+ * own regardless would refuse every client a release behind for no
142
+ * reason. A version this cannot speak gets the newest it can, which is
143
+ * the offer the client then accepts or drops.
144
+ */
145
+ const asked = bag(message.params).protocolVersion;
95
146
  return ok({
96
- protocolVersion: PROTOCOL,
147
+ protocolVersion: typeof asked === 'string' && SPOKEN.includes(asked) ? asked : PROTOCOL,
97
148
  capabilities: { tools: { listChanged: false } },
98
149
  serverInfo: { name: options.name, version: options.version },
99
150
  });
@@ -101,14 +152,14 @@ export async function answer(host, message, options) {
101
152
  if (method === 'ping')
102
153
  return ok({});
103
154
  if (method === 'tools/list')
104
- return ok(listing());
155
+ return ok(listing(options.groups));
105
156
  if (method === 'tools/call') {
106
157
  const params = bag(message.params);
107
158
  const name = typeof params.name === 'string' ? params.name : '';
108
159
  if (name === '')
109
160
  return no(INVALID_PARAMS, 'tools/call needs a name.');
110
161
  try {
111
- return ok(await call(host, name, params.arguments));
162
+ return ok(await call(host, name, params.arguments, reporter(message, options.notify), options.groups));
112
163
  }
113
164
  catch (error) {
114
165
  return no(INTERNAL, error instanceof Error ? error.message : String(error));
@@ -9,5 +9,6 @@ import type { HostConnection } from '../ahp/connection.js';
9
9
  export declare function stdio(host: HostConnection, options: {
10
10
  name: string;
11
11
  version: string;
12
+ groups?: readonly string[];
12
13
  onProblem?(said: string): void;
13
14
  }): Promise<void>;
@@ -34,7 +34,13 @@ export async function stdio(host, options) {
34
34
  options.onProblem?.(`Not JSON: ${said.slice(0, 200)}`);
35
35
  return;
36
36
  }
37
- const reply = await answer(host, message, options);
37
+ const reply = await answer(host, message, {
38
+ ...options,
39
+ // A notification mid-request is free here: stdout is a stream and
40
+ // the client is already reading lines off it. The HTTP half has to
41
+ // choose a response type before it can say anything at all.
42
+ notify: (notification) => say({ jsonrpc: '2.0', ...notification }),
43
+ });
38
44
  if (reply !== undefined)
39
45
  say(reply);
40
46
  }).catch((error) => {
@@ -15,10 +15,28 @@ export interface Schema {
15
15
  required?: string[];
16
16
  additionalProperties?: boolean;
17
17
  }
18
+ /**
19
+ * A set of tools a caller has to ask for by name.
20
+ *
21
+ * The core table - sessions, turns, attention - is what a server is for and is
22
+ * always served. These are not: a tool table is read by a model alongside
23
+ * everything else it was given, and thirty tools is a worse server than twelve
24
+ * for the thing almost everybody wants. So they are opt-*in*, one group at a
25
+ * time, and a caller that needs files says so.
26
+ */
27
+ export type Group = 'resources' | 'terminals' | 'automations' | 'changes';
28
+ /** Every group there is, for `--mcp-tools` to name in its help. */
29
+ export declare const GROUPS: readonly Group[];
18
30
  /** One tool, as MCP describes it and as this client runs it. */
19
31
  export interface Tool {
20
32
  /** The name a caller uses. MCP has no dots, so these are underscored. */
21
33
  name: string;
34
+ /**
35
+ * The group that has to be turned on for this to be served.
36
+ *
37
+ * Absent is core: always there, never asked for.
38
+ */
39
+ group?: Group;
22
40
  /** A short label, for a client that shows one. */
23
41
  title: string;
24
42
  /** What it does and when to reach for it, written for a model. */
@@ -26,8 +44,25 @@ export interface Tool {
26
44
  input: Schema;
27
45
  /** Whether it changes anything, which some clients ask a person about. */
28
46
  readOnly: boolean;
29
- run(host: HostConnection, input: Record<string, unknown>): Promise<unknown>;
47
+ /**
48
+ * Do it.
49
+ *
50
+ * `report` is where to say what is happening while a long one runs, and is
51
+ * absent unless the caller asked for progress and the transport can carry
52
+ * it. A tool that reports nothing is a tool that finishes quickly enough
53
+ * not to need to.
54
+ */
55
+ run(host: HostConnection, input: Record<string, unknown>, report?: (said: string) => void): Promise<unknown>;
30
56
  }
31
57
  export declare const TOOLS: Tool[];
32
- /** One tool by the name a caller used, or nothing. */
58
+ /**
59
+ * One tool by the name a caller used, or nothing.
60
+ *
61
+ * Over the whole table, including groups nobody turned on - whether a tool
62
+ * exists and whether this server serves it are different questions, and
63
+ * `served` answers the second. Telling somebody the tool is in a group they
64
+ * did not ask for is a better answer than telling them it does not exist.
65
+ */
33
66
  export declare const named: (name: string) => Tool | undefined;
67
+ /** The tools a server started with these groups serves. */
68
+ export declare const served: (groups?: readonly string[]) => Tool[];