toolcraft 0.0.2 → 0.0.3

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 (85) hide show
  1. package/README.md +458 -58
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +768 -40
  5. package/dist/human-in-loop/approval-tasks.d.ts +31 -0
  6. package/dist/human-in-loop/approval-tasks.js +201 -0
  7. package/dist/human-in-loop/approvals-commands.d.ts +11 -0
  8. package/dist/human-in-loop/approvals-commands.js +191 -0
  9. package/dist/human-in-loop/config.d.ts +11 -0
  10. package/dist/human-in-loop/config.js +21 -0
  11. package/dist/human-in-loop/default-provider.d.ts +2 -0
  12. package/dist/human-in-loop/default-provider.js +26 -0
  13. package/dist/human-in-loop/gate.d.ts +4 -0
  14. package/dist/human-in-loop/gate.js +57 -0
  15. package/dist/human-in-loop/index.d.ts +7 -0
  16. package/dist/human-in-loop/index.js +4 -0
  17. package/dist/human-in-loop/runner.d.ts +3 -0
  18. package/dist/human-in-loop/runner.js +196 -0
  19. package/dist/human-in-loop/spawn.d.ts +3 -0
  20. package/dist/human-in-loop/spawn.js +16 -0
  21. package/dist/human-in-loop/state-machine.d.ts +4 -0
  22. package/dist/human-in-loop/state-machine.js +10 -0
  23. package/dist/human-in-loop/types.d.ts +41 -0
  24. package/dist/human-in-loop/types.js +13 -0
  25. package/dist/index.compile-check.js +24 -0
  26. package/dist/index.d.ts +32 -13
  27. package/dist/index.js +82 -17
  28. package/dist/json-schema-converter.d.ts +21 -0
  29. package/dist/json-schema-converter.js +432 -0
  30. package/dist/mcp-proxy.d.ts +8 -0
  31. package/dist/mcp-proxy.js +383 -0
  32. package/dist/mcp.compile-check.js +1 -0
  33. package/dist/mcp.d.ts +2 -0
  34. package/dist/mcp.js +103 -11
  35. package/dist/sdk.compile-check.js +77 -0
  36. package/dist/sdk.d.ts +14 -5
  37. package/dist/sdk.js +57 -6
  38. package/dist/user-error.d.ts +3 -0
  39. package/dist/user-error.js +6 -0
  40. package/node_modules/@poe-code/agent-human-in-loop/README.md +42 -0
  41. package/node_modules/@poe-code/agent-human-in-loop/dist/index.d.ts +5 -0
  42. package/node_modules/@poe-code/agent-human-in-loop/dist/index.js +3 -0
  43. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.d.ts +2 -0
  44. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +11 -0
  45. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.d.ts +4 -0
  46. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +40 -0
  47. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.d.ts +6 -0
  48. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +33 -0
  49. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.d.ts +4 -0
  50. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +4 -0
  51. package/node_modules/@poe-code/agent-human-in-loop/dist/types.d.ts +14 -0
  52. package/node_modules/@poe-code/agent-human-in-loop/dist/types.js +1 -0
  53. package/node_modules/@poe-code/agent-human-in-loop/package.json +25 -0
  54. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +6 -0
  55. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +175 -0
  56. package/node_modules/@poe-code/agent-mcp-config/dist/configs.d.ts +22 -0
  57. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +74 -0
  58. package/node_modules/@poe-code/agent-mcp-config/dist/index.d.ts +3 -0
  59. package/node_modules/@poe-code/agent-mcp-config/dist/index.js +2 -0
  60. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +31 -0
  61. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +87 -0
  62. package/node_modules/@poe-code/agent-mcp-config/dist/types.d.ts +25 -0
  63. package/node_modules/@poe-code/agent-mcp-config/dist/types.js +1 -0
  64. package/node_modules/@poe-code/agent-mcp-config/package.json +25 -0
  65. package/node_modules/@poe-code/task-list/README.md +114 -0
  66. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.d.ts +2 -0
  67. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +466 -0
  68. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +8 -0
  69. package/node_modules/@poe-code/task-list/dist/backends/utils.js +58 -0
  70. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.d.ts +2 -0
  71. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +444 -0
  72. package/node_modules/@poe-code/task-list/dist/index.d.ts +4 -0
  73. package/node_modules/@poe-code/task-list/dist/index.js +4 -0
  74. package/node_modules/@poe-code/task-list/dist/open.d.ts +3 -0
  75. package/node_modules/@poe-code/task-list/dist/open.js +34 -0
  76. package/node_modules/@poe-code/task-list/dist/schema/store.schema.json +32 -0
  77. package/node_modules/@poe-code/task-list/dist/schema/task.schema.json +33 -0
  78. package/node_modules/@poe-code/task-list/dist/state-machine.d.ts +16 -0
  79. package/node_modules/@poe-code/task-list/dist/state-machine.js +67 -0
  80. package/node_modules/@poe-code/task-list/dist/state.d.ts +29 -0
  81. package/node_modules/@poe-code/task-list/dist/state.js +61 -0
  82. package/node_modules/@poe-code/task-list/dist/types.d.ts +116 -0
  83. package/node_modules/@poe-code/task-list/dist/types.js +37 -0
  84. package/node_modules/@poe-code/task-list/package.json +26 -0
  85. package/package.json +22 -7
package/dist/sdk.d.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  import type { ObjectSchema, Static } from "toolcraft-schema";
2
2
  import type { Group, Scope } from "./index.js";
3
+ import type { HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
3
4
  type ScopeInput = readonly Scope[] | undefined;
5
+ type HumanInLoopMode = "sync" | "async";
6
+ type HumanInLoopModeInput = HumanInLoopMode | null | undefined;
4
7
  type Primitive = string | number | boolean | bigint | symbol | null | undefined;
5
8
  type EmptyRecord = Record<never, never>;
6
9
  type EffectiveCommandScope<TOwnScope extends ScopeInput, TInheritedScope extends ScopeInput> = TOwnScope extends readonly Scope[] ? TOwnScope : TInheritedScope extends readonly Scope[] ? TInheritedScope : readonly ["cli", "sdk"];
7
10
  type EffectiveGroupScope<TOwnScope extends ScopeInput, TInheritedScope extends ScopeInput> = TOwnScope extends readonly Scope[] ? TOwnScope : TInheritedScope extends readonly Scope[] ? TInheritedScope : undefined;
8
11
  type IncludesSDK<TScope> = TScope extends readonly Scope[] ? "sdk" extends TScope[number] ? true : false : false;
12
+ type EffectiveCommandHumanInLoopMode<TOwnHumanInLoopMode extends HumanInLoopModeInput, TInheritedHumanInLoopMode extends HumanInLoopMode | undefined> = TOwnHumanInLoopMode extends HumanInLoopMode ? TOwnHumanInLoopMode : TOwnHumanInLoopMode extends null ? undefined : TInheritedHumanInLoopMode;
13
+ type EffectiveGroupHumanInLoopMode<TOwnHumanInLoopMode extends HumanInLoopModeInput, TInheritedHumanInLoopMode extends HumanInLoopMode | undefined> = TOwnHumanInLoopMode extends HumanInLoopMode ? TOwnHumanInLoopMode : TOwnHumanInLoopMode extends null ? undefined : TInheritedHumanInLoopMode;
9
14
  type Separator = "-" | "_" | " " | ".";
10
15
  type IsUppercase<TValue extends string> = TValue extends Uppercase<TValue> ? TValue extends Lowercase<TValue> ? false : true : false;
11
16
  type IsLowercase<TValue extends string> = TValue extends Lowercase<TValue> ? TValue extends Uppercase<TValue> ? false : true : false;
@@ -24,40 +29,44 @@ type CamelCase<TValue extends string> = JoinCamelWords<SplitCamelWords<TValue>>;
24
29
  type Camelize<TValue> = TValue extends Primitive ? TValue : TValue extends readonly (infer TItem)[] ? Array<Camelize<TItem>> : TValue extends object ? {
25
30
  [TKey in keyof TValue as TKey extends string ? CamelCase<TKey> : TKey]: Camelize<TValue[TKey]>;
26
31
  } : TValue;
32
+ type SDKResult<TResult, THumanInLoopMode extends HumanInLoopMode | undefined> = THumanInLoopMode extends "async" ? HumanInLoopPending : TResult;
27
33
  type SDKMethod<TParamsSchema extends ObjectSchema<any>, TResult> = (params: Camelize<Static<TParamsSchema>>) => Promise<TResult>;
28
34
  type UnionToIntersection<TValue> = (TValue extends unknown ? (value: TValue) => void : never) extends (value: infer TResult) => void ? TResult : never;
29
35
  type Simplify<TValue> = {
30
36
  [TKey in keyof TValue]: TValue[TKey];
31
37
  };
32
38
  type RawChildrenValue<TChildren> = TChildren extends readonly unknown[] ? TChildren[number] : never;
33
- type SDKNodeShape<TNode, TInheritedScope extends ScopeInput> = TNode extends {
39
+ type SDKNodeShape<TNode, TInheritedScope extends ScopeInput, TInheritedHumanInLoopMode extends HumanInLoopMode | undefined> = TNode extends {
34
40
  kind: "command";
35
41
  readonly __agentKitCommandTypeInfo: {
36
42
  name: infer TName extends string;
37
43
  params: infer TParamsSchema extends ObjectSchema<any>;
38
44
  result: infer TResult;
39
45
  ownScope: infer TOwnScope extends ScopeInput;
46
+ ownHumanInLoopMode: infer TOwnHumanInLoopMode extends HumanInLoopModeInput;
40
47
  };
41
48
  } ? IncludesSDK<EffectiveCommandScope<TOwnScope, TInheritedScope>> extends true ? {
42
- [TKey in CamelCase<TName>]: SDKMethod<TParamsSchema, TResult>;
49
+ [TKey in CamelCase<TName>]: SDKMethod<TParamsSchema, SDKResult<TResult, EffectiveCommandHumanInLoopMode<TOwnHumanInLoopMode, TInheritedHumanInLoopMode>>>;
43
50
  } : EmptyRecord : TNode extends {
44
51
  kind: "group";
45
52
  readonly __agentKitGroupTypeInfo: {
46
53
  name: infer TName extends string;
47
54
  children: infer TChildren extends readonly unknown[];
48
55
  ownScope: infer TOwnScope extends ScopeInput;
56
+ ownHumanInLoopMode: infer TOwnHumanInLoopMode extends HumanInLoopModeInput;
49
57
  };
50
- } ? SDKChildrenShape<TChildren, EffectiveGroupScope<TOwnScope, TInheritedScope>> extends infer TChildShape extends object ? keyof TChildShape extends never ? EmptyRecord : {
58
+ } ? SDKChildrenShape<TChildren, EffectiveGroupScope<TOwnScope, TInheritedScope>, EffectiveGroupHumanInLoopMode<TOwnHumanInLoopMode, TInheritedHumanInLoopMode>> extends infer TChildShape extends object ? keyof TChildShape extends never ? EmptyRecord : {
51
59
  [TKey in CamelCase<TName>]: TChildShape;
52
60
  } : never : EmptyRecord;
53
- type SDKChildrenShape<TChildren, TInheritedScope extends ScopeInput> = Simplify<UnionToIntersection<SDKNodeShape<RawChildrenValue<TChildren>, TInheritedScope>>>;
61
+ type SDKChildrenShape<TChildren, TInheritedScope extends ScopeInput, TInheritedHumanInLoopMode extends HumanInLoopMode | undefined> = Simplify<UnionToIntersection<SDKNodeShape<RawChildrenValue<TChildren>, TInheritedScope, TInheritedHumanInLoopMode>>>;
54
62
  export interface CreateSDKOptions<TServices extends object = Record<string, unknown>> {
55
63
  services?: TServices;
56
64
  casing?: "camel";
65
+ humanInLoop?: HumanInLoopRuntimeOptions;
57
66
  }
58
67
  export declare function createSDK<TRootInfo, TServices extends object = Record<string, unknown>>(root: Group<any> & {
59
68
  readonly __agentKitGroupTypeInfo: TRootInfo;
60
69
  }, options?: CreateSDKOptions<TServices>): TRootInfo extends {
61
70
  children: infer TChildren extends readonly unknown[];
62
- } ? SDKChildrenShape<TChildren, undefined> : EmptyRecord;
71
+ } ? SDKChildrenShape<TChildren, undefined, undefined> : EmptyRecord;
63
72
  export {};
package/dist/sdk.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { access, readFile, writeFile } from "node:fs/promises";
2
2
  import { UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
3
+ import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
4
+ import { invokeWithHumanInLoop } from "./human-in-loop/index.js";
5
+ import { hasMcpProxyGroups, resolveMcpProxies } from "./mcp-proxy.js";
3
6
  import { getExpectedNumberDescription, isValidNumberSchemaValue } from "./number-schema.js";
4
7
  import { filterSchemaForScope } from "./schema-scope.js";
5
8
  const RESERVED_SERVICE_NAMES = new Set(["params", "secrets", "fetch", "fs", "env", "progress"]);
@@ -169,15 +172,25 @@ function defineMember(target, key, value) {
169
172
  });
170
173
  }
171
174
  export function createSDK(root, options = {}) {
175
+ const mergedRoot = mergeApprovalsGroup(root);
176
+ if (!hasMcpProxyGroups(mergedRoot)) {
177
+ return createResolvedSDK(mergedRoot, options);
178
+ }
179
+ return createDeferredSDK(mergedRoot, options);
180
+ }
181
+ function createResolvedSDK(root, options = {}) {
172
182
  const services = options.services ?? {};
183
+ const runtimeOptions = options.humanInLoop ?? {};
173
184
  void options.casing;
174
185
  validateServices(services);
175
- function build(node) {
186
+ function build(node, path) {
176
187
  if (node.kind === "command") {
177
188
  return async (params) => {
178
189
  const secrets = resolveCommandSecrets(node);
179
190
  const baseContext = {
180
191
  ...services,
192
+ runtimeOptions,
193
+ root,
181
194
  secrets,
182
195
  fetch: globalThis.fetch,
183
196
  fs: createFs(),
@@ -192,27 +205,65 @@ export function createSDK(root, options = {}) {
192
205
  throw new Error(`Bug: command "${node.name}" must define an object params schema for SDK.`);
193
206
  }
194
207
  const validatedParams = validateSDKArguments(paramsSchema, params);
195
- return node.handler({
208
+ return invokeWithHumanInLoop(node, {
196
209
  ...baseContext,
197
210
  params: validatedParams,
198
- });
211
+ }, runtimeOptions, [...path, node.name].join("."));
199
212
  };
200
213
  }
201
214
  const output = {};
215
+ const nextPath = node === root ? path : [...path, node.name];
202
216
  for (const child of node.children) {
203
217
  if (child.kind === "command") {
204
218
  if (!child.scope.includes("sdk")) {
205
219
  continue;
206
220
  }
207
- defineMember(output, formatSegment(child.name), build(child));
221
+ defineMember(output, formatSegment(child.name), build(child, nextPath));
208
222
  continue;
209
223
  }
210
- const childValue = build(child);
224
+ const childValue = build(child, nextPath);
211
225
  if (isPlainObject(childValue) && Object.keys(childValue).length > 0) {
212
226
  defineMember(output, formatSegment(child.name), childValue);
213
227
  }
214
228
  }
215
229
  return output;
216
230
  }
217
- return build(root);
231
+ return build(root, []);
232
+ }
233
+ function createDeferredSDK(root, options) {
234
+ let sdkPromise;
235
+ const resolveSDK = () => {
236
+ sdkPromise ??= (async () => {
237
+ await resolveMcpProxies(root);
238
+ return createResolvedSDK(root, options);
239
+ })();
240
+ return sdkPromise;
241
+ };
242
+ const resolvePath = async (path) => {
243
+ let current = await resolveSDK();
244
+ for (const segment of path) {
245
+ if (typeof segment !== "string" && typeof segment !== "number") {
246
+ return undefined;
247
+ }
248
+ current = current[segment];
249
+ }
250
+ return current;
251
+ };
252
+ const createPathProxy = (path) => new Proxy(() => undefined, {
253
+ apply(_target, _thisArg, argumentsList) {
254
+ return resolvePath(path).then((value) => {
255
+ if (typeof value !== "function") {
256
+ throw new TypeError(`SDK member "${path.map(String).join(".")}" is not callable.`);
257
+ }
258
+ return value(...argumentsList);
259
+ });
260
+ },
261
+ get(_target, property) {
262
+ if (property === "then") {
263
+ return path.length === 0 ? resolveSDK().then.bind(resolveSDK()) : undefined;
264
+ }
265
+ return createPathProxy([...path, property]);
266
+ },
267
+ });
268
+ return createPathProxy([]);
218
269
  }
@@ -0,0 +1,3 @@
1
+ export declare class UserError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class UserError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "UserError";
5
+ }
6
+ }
@@ -0,0 +1,42 @@
1
+ ## Overview
2
+
3
+ `@poe-code/agent-human-in-loop` is a UI-only package for asking a human "approve this?" before an agent proceeds. The UI is providerized, sync vs async is the caller's choice because the API returns a Promise you can await immediately or hold and resolve later, and the package is approval-only: decline returns a declined outcome and can optionally capture a reason.
4
+
5
+ ## API
6
+
7
+ ```ts
8
+ declare function requestApproval(
9
+ args: ApprovalRequest & { provider: HumanInLoopProvider }
10
+ ): Promise<ApprovalResult>;
11
+
12
+ declare function osascriptProvider(options?: OsascriptProviderOptions): HumanInLoopProvider;
13
+
14
+ declare function mockProvider(
15
+ answer: ApprovalResult | (() => ApprovalResult | Promise<ApprovalResult>)
16
+ ): HumanInLoopProvider;
17
+ ```
18
+
19
+ ## Providers
20
+
21
+ - `osascriptProvider({ title?, binary? })` — macOS native dialog via `display dialog`. Mac only.
22
+ - `mockProvider(answer | thunk)` — fixed or scripted answers for tests.
23
+
24
+ ## Env vars
25
+
26
+ None in v1.
27
+
28
+ ## AppleScript escaping note
29
+
30
+ Messages and prompts are passed verbatim through the dialog; the provider escapes `"` and `\` for AppleScript string literals. Do not pass user-supplied AppleScript fragments expecting them to execute.
31
+
32
+ ## Example
33
+
34
+ ```ts
35
+ import { requestApproval, osascriptProvider } from "@poe-code/agent-human-in-loop";
36
+
37
+ const result = await requestApproval({
38
+ message: "Run `rm -rf /tmp/foo`?",
39
+ declineInputPrompt: "Why decline?",
40
+ provider: osascriptProvider({ title: "Claude" })
41
+ });
42
+ ```
@@ -0,0 +1,5 @@
1
+ export type { ApprovalRequest, ApprovalResult, HumanInLoopProvider, } from "./types.js";
2
+ export { requestApproval } from "./request-approval.js";
3
+ export { osascriptProvider } from "./providers/osascript.js";
4
+ export type { OsascriptProviderOptions } from "./providers/osascript.js";
5
+ export { mockProvider } from "./providers/mock.js";
@@ -0,0 +1,3 @@
1
+ export { requestApproval } from "./request-approval.js";
2
+ export { osascriptProvider } from "./providers/osascript.js";
3
+ export { mockProvider } from "./providers/mock.js";
@@ -0,0 +1,2 @@
1
+ import type { ApprovalResult, HumanInLoopProvider } from "../types.js";
2
+ export declare function mockProvider(answer: ApprovalResult | (() => ApprovalResult | Promise<ApprovalResult>)): HumanInLoopProvider;
@@ -0,0 +1,11 @@
1
+ export function mockProvider(answer) {
2
+ return {
3
+ id: "mock",
4
+ async requestApproval(_request) {
5
+ if (typeof answer === "function") {
6
+ return await answer();
7
+ }
8
+ return answer;
9
+ },
10
+ };
11
+ }
@@ -0,0 +1,4 @@
1
+ import type { ApprovalRequest, ApprovalResult } from "../types.js";
2
+ export declare function escapeAppleScriptString(value: string): string;
3
+ export declare function buildScript(request: ApprovalRequest, title: string): string;
4
+ export declare function parseStdout(out: string): ApprovalResult;
@@ -0,0 +1,40 @@
1
+ export function escapeAppleScriptString(value) {
2
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3
+ }
4
+ export function buildScript(request, title) {
5
+ const escapedMessage = escapeAppleScriptString(request.message);
6
+ const escapedTitle = escapeAppleScriptString(title);
7
+ if (request.declineInputPrompt === undefined) {
8
+ return `button returned of (display dialog "${escapedMessage}" with title "${escapedTitle}" buttons {"Decline","Approve"} default button "Approve")`;
9
+ }
10
+ const escapedPrompt = escapeAppleScriptString(request.declineInputPrompt);
11
+ return `set firstResp to button returned of (display dialog "${escapedMessage}" with title "${escapedTitle}" buttons {"Decline","Approve"} default button "Approve")
12
+ if firstResp is "Approve" then
13
+ return "APPROVED"
14
+ end if
15
+ try
16
+ set reason to text returned of (display dialog "${escapedPrompt}" default answer "" with title "${escapedTitle}" buttons {"Cancel","Submit"} default button "Submit")
17
+ return "DECLINED:" & reason
18
+ on error number -128
19
+ return "DECLINED:"
20
+ end try`;
21
+ }
22
+ export function parseStdout(out) {
23
+ const value = out.replace(/\n+$/u, "");
24
+ switch (value) {
25
+ case "Approve":
26
+ case "APPROVED":
27
+ return { outcome: "approved" };
28
+ case "Decline":
29
+ return { outcome: "declined" };
30
+ default:
31
+ break;
32
+ }
33
+ if (value.startsWith("DECLINED:")) {
34
+ const reason = value.slice("DECLINED:".length);
35
+ return reason === ""
36
+ ? { outcome: "declined" }
37
+ : { outcome: "declined", reason };
38
+ }
39
+ throw new Error(`unexpected osascript output: ${out}`);
40
+ }
@@ -0,0 +1,6 @@
1
+ import type { HumanInLoopProvider } from "../types.js";
2
+ export interface OsascriptProviderOptions {
3
+ title?: string;
4
+ binary?: string;
5
+ }
6
+ export declare function osascriptProvider(options?: OsascriptProviderOptions): HumanInLoopProvider;
@@ -0,0 +1,33 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { buildScript, parseStdout } from "./osascript-script.js";
4
+ const execFileAsync = promisify(execFile);
5
+ function isUserCanceled(error) {
6
+ const message = error instanceof Error ? error.message : error === undefined ? "" : String(error);
7
+ const stderr = error?.stderr ?? "";
8
+ return `${message}\n${stderr}`.includes("(-128)");
9
+ }
10
+ export function osascriptProvider(options = {}) {
11
+ const title = options.title ?? "Approval needed";
12
+ const binary = options.binary ?? "osascript";
13
+ return {
14
+ id: "osascript",
15
+ async requestApproval(request) {
16
+ const script = buildScript(request, title);
17
+ try {
18
+ const { stdout } = await execFileAsync(binary, ["-e", script]);
19
+ return parseStdout(stdout);
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT") {
23
+ throw new Error("osascript not found — provide a different provider on this platform");
24
+ }
25
+ if (isUserCanceled(error)) {
26
+ return { outcome: "declined" };
27
+ }
28
+ const stderr = error.stderr ?? String(error);
29
+ throw new Error(`osascript failed: ${stderr.trim()}`);
30
+ }
31
+ }
32
+ };
33
+ }
@@ -0,0 +1,4 @@
1
+ import type { ApprovalRequest, ApprovalResult, HumanInLoopProvider } from "./types.js";
2
+ export declare function requestApproval(args: ApprovalRequest & {
3
+ provider: HumanInLoopProvider;
4
+ }): Promise<ApprovalResult>;
@@ -0,0 +1,4 @@
1
+ export function requestApproval(args) {
2
+ const { provider, ...request } = args;
3
+ return provider.requestApproval(request);
4
+ }
@@ -0,0 +1,14 @@
1
+ export interface ApprovalRequest {
2
+ message: string;
3
+ declineInputPrompt?: string;
4
+ }
5
+ export type ApprovalResult = {
6
+ outcome: "approved";
7
+ } | {
8
+ outcome: "declined";
9
+ reason?: string;
10
+ };
11
+ export interface HumanInLoopProvider {
12
+ readonly id: string;
13
+ requestApproval(request: ApprovalRequest): Promise<ApprovalResult>;
14
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@poe-code/agent-human-in-loop",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "cd ../.. && vitest run --passWithNoTests packages/agent-human-in-loop",
17
+ "typecheck": "tsc --noEmit",
18
+ "dev": "tsx example.ts"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "dependencies": {},
24
+ "devDependencies": {}
25
+ }
@@ -0,0 +1,6 @@
1
+ import type { McpServerEntry, ApplyOptions } from "./types.js";
2
+ export declare class UnsupportedAgentError extends Error {
3
+ constructor(agentId: string);
4
+ }
5
+ export declare function configure(agentId: string, server: McpServerEntry, options: ApplyOptions): Promise<void>;
6
+ export declare function unconfigure(agentId: string, serverName: string, options: ApplyOptions): Promise<void>;
@@ -0,0 +1,175 @@
1
+ import path from "node:path";
2
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
3
+ import { runMutations, configMutation, fileMutation, readFileIfExists } from "@poe-code/config-mutations";
4
+ import { getAgentConfig, resolveConfigPath, isSupported } from "./configs.js";
5
+ import { getShapeTransformer } from "./shapes.js";
6
+ function getConfigDirectory(configPath) {
7
+ return path.dirname(configPath);
8
+ }
9
+ export class UnsupportedAgentError extends Error {
10
+ constructor(agentId) {
11
+ super(`Unsupported agent: ${agentId}`);
12
+ this.name = "UnsupportedAgentError";
13
+ }
14
+ }
15
+ function isConfigObject(value) {
16
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
17
+ }
18
+ function resolveServerMap(document, configKey) {
19
+ const value = document[configKey];
20
+ return isConfigObject(value) ? value : {};
21
+ }
22
+ function mergeServerMap(document, configKey, servers) {
23
+ return { ...document, [configKey]: servers };
24
+ }
25
+ function expandHomePath(configPath, homeDir) {
26
+ if (!configPath.startsWith("~")) {
27
+ return configPath;
28
+ }
29
+ if (configPath === "~") {
30
+ return homeDir;
31
+ }
32
+ if (configPath.startsWith("~/")) {
33
+ return path.join(homeDir, configPath.slice(2));
34
+ }
35
+ return path.join(homeDir, configPath.slice(1));
36
+ }
37
+ function parseYamlDocument(content) {
38
+ if (content.trim() === "") {
39
+ return {};
40
+ }
41
+ const parsed = parseYaml(content);
42
+ if (parsed === null || parsed === undefined) {
43
+ return {};
44
+ }
45
+ if (!isConfigObject(parsed)) {
46
+ throw new Error("Expected YAML document to be an object.");
47
+ }
48
+ return parsed;
49
+ }
50
+ function serializeYamlDocument(document) {
51
+ const serialized = stringifyYaml(document);
52
+ return serialized.endsWith("\n") ? serialized : `${serialized}\n`;
53
+ }
54
+ async function readYamlConfig(configPath, options) {
55
+ const absolutePath = expandHomePath(configPath, options.homeDir);
56
+ const existingContent = await readFileIfExists(options.fs, absolutePath);
57
+ if (existingContent === null) {
58
+ return {};
59
+ }
60
+ return parseYamlDocument(existingContent);
61
+ }
62
+ async function writeYamlConfig(configPath, document, options) {
63
+ if (options.dryRun) {
64
+ return;
65
+ }
66
+ const absolutePath = expandHomePath(configPath, options.homeDir);
67
+ const configDir = path.dirname(absolutePath);
68
+ await options.fs.mkdir(configDir, { recursive: true });
69
+ await options.fs.writeFile(absolutePath, serializeYamlDocument(document), {
70
+ encoding: "utf8"
71
+ });
72
+ }
73
+ function removeServer(document, configKey, serverName) {
74
+ const servers = resolveServerMap(document, configKey);
75
+ if (!(serverName in servers)) {
76
+ return { changed: false, content: document };
77
+ }
78
+ const nextServers = { ...servers };
79
+ delete nextServers[serverName];
80
+ if (Object.keys(nextServers).length === 0) {
81
+ const nextDocument = { ...document };
82
+ delete nextDocument[configKey];
83
+ return { changed: true, content: nextDocument };
84
+ }
85
+ return {
86
+ changed: true,
87
+ content: mergeServerMap(document, configKey, nextServers)
88
+ };
89
+ }
90
+ export async function configure(agentId, server, options) {
91
+ if (!isSupported(agentId)) {
92
+ throw new UnsupportedAgentError(agentId);
93
+ }
94
+ const config = getAgentConfig(agentId);
95
+ const configPath = resolveConfigPath(config, options.platform);
96
+ const shapeTransformer = getShapeTransformer(config.shape);
97
+ const shaped = shapeTransformer(server);
98
+ if (shaped === undefined) {
99
+ await unconfigure(agentId, server.name, options);
100
+ return;
101
+ }
102
+ if (config.format === "yaml") {
103
+ const document = await readYamlConfig(configPath, options);
104
+ const servers = resolveServerMap(document, config.configKey);
105
+ const nextDocument = mergeServerMap(document, config.configKey, {
106
+ ...servers,
107
+ [server.name]: shaped
108
+ });
109
+ await writeYamlConfig(configPath, nextDocument, options);
110
+ return;
111
+ }
112
+ const configDir = getConfigDirectory(configPath);
113
+ await runMutations([
114
+ fileMutation.ensureDirectory({
115
+ path: configDir,
116
+ label: `Ensure directory ${configDir}`
117
+ }),
118
+ // Use transform to replace the server entry entirely (not deep-merge)
119
+ // This ensures old fields like 'args' are removed when switching to array 'command'
120
+ configMutation.transform({
121
+ target: configPath,
122
+ format: config.format,
123
+ transform: (document) => {
124
+ const servers = resolveServerMap(document, config.configKey);
125
+ const newServers = {
126
+ ...servers,
127
+ [server.name]: shaped
128
+ };
129
+ return {
130
+ changed: true,
131
+ content: mergeServerMap(document, config.configKey, newServers)
132
+ };
133
+ },
134
+ label: `Add ${server.name} to ${configPath}`
135
+ })
136
+ ], {
137
+ fs: options.fs,
138
+ homeDir: options.homeDir,
139
+ dryRun: options.dryRun,
140
+ observers: options.observers
141
+ });
142
+ }
143
+ export async function unconfigure(agentId, serverName, options) {
144
+ if (!isSupported(agentId)) {
145
+ throw new UnsupportedAgentError(agentId);
146
+ }
147
+ const config = getAgentConfig(agentId);
148
+ const configPath = resolveConfigPath(config, options.platform);
149
+ if (config.format === "yaml") {
150
+ const document = await readYamlConfig(configPath, options);
151
+ const { changed, content } = removeServer(document, config.configKey, serverName);
152
+ if (!changed) {
153
+ return;
154
+ }
155
+ await writeYamlConfig(configPath, content, options);
156
+ return;
157
+ }
158
+ await runMutations([
159
+ configMutation.prune({
160
+ target: configPath,
161
+ format: config.format,
162
+ shape: {
163
+ [config.configKey]: {
164
+ [serverName]: {}
165
+ }
166
+ },
167
+ label: `Remove ${serverName} from ${configPath}`
168
+ })
169
+ ], {
170
+ fs: options.fs,
171
+ homeDir: options.homeDir,
172
+ dryRun: options.dryRun,
173
+ observers: options.observers
174
+ });
175
+ }
@@ -0,0 +1,22 @@
1
+ import type { ShapeName } from "./shapes.js";
2
+ export type ConfigFormat = "json" | "toml" | "yaml";
3
+ export type Platform = "darwin" | "linux" | "win32";
4
+ export interface AgentMcpConfig {
5
+ configFile: string | ((platform: Platform) => string);
6
+ configKey: string;
7
+ format: ConfigFormat;
8
+ shape: ShapeName;
9
+ mcpOutputFormat?: string;
10
+ }
11
+ export declare const supportedAgents: readonly string[];
12
+ export type AgentSupportStatus = "supported" | "unsupported" | "unknown";
13
+ export interface AgentSupportResult {
14
+ status: AgentSupportStatus;
15
+ input: string;
16
+ id?: string;
17
+ config?: AgentMcpConfig;
18
+ }
19
+ export declare function resolveAgentSupport(input: string, registry?: Record<string, AgentMcpConfig>): AgentSupportResult;
20
+ export declare function isSupported(agentId: string): boolean;
21
+ export declare function getAgentConfig(agentId: string): AgentMcpConfig | undefined;
22
+ export declare function resolveConfigPath(config: AgentMcpConfig, platform: Platform): string;