@kmmao/happy-agent 0.7.1 → 0.7.2
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/dist/index.cjs +148 -50
- package/dist/index.d.cts +50 -5
- package/dist/index.d.mts +50 -5
- package/dist/index.mjs +148 -50
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -39,7 +39,7 @@ function _interopNamespaceDefault(e) {
|
|
|
39
39
|
|
|
40
40
|
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
|
|
41
41
|
|
|
42
|
-
var version = "0.7.
|
|
42
|
+
var version = "0.7.2";
|
|
43
43
|
|
|
44
44
|
function loadConfig() {
|
|
45
45
|
const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://s.sangreal.code.xycloud.info:2443").replace(/\/+$/, "");
|
|
@@ -157,6 +157,21 @@ function decrypt(key, variant, data) {
|
|
|
157
157
|
return decryptWithDataKey(data, key);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
function createCipher(key, variant) {
|
|
161
|
+
return {
|
|
162
|
+
encrypt(data) {
|
|
163
|
+
return encodeBase64(encrypt(key, variant, data));
|
|
164
|
+
},
|
|
165
|
+
decrypt(data) {
|
|
166
|
+
try {
|
|
167
|
+
const value = decrypt(key, variant, decodeBase64(data));
|
|
168
|
+
return value === null ? { ok: false } : { ok: true, value };
|
|
169
|
+
} catch {
|
|
170
|
+
return { ok: false };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
160
175
|
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
161
176
|
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
162
177
|
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
@@ -639,16 +654,14 @@ async function withBackoff(fn, options) {
|
|
|
639
654
|
class RpcHandlerManager {
|
|
640
655
|
handlers = /* @__PURE__ */ new Map();
|
|
641
656
|
scopePrefix;
|
|
642
|
-
|
|
643
|
-
encryptionVariant;
|
|
657
|
+
cipher;
|
|
644
658
|
logger;
|
|
645
659
|
socket = null;
|
|
646
660
|
reregisterInterval = null;
|
|
647
661
|
fastRetryTimer = null;
|
|
648
662
|
constructor(config) {
|
|
649
663
|
this.scopePrefix = config.scopePrefix;
|
|
650
|
-
this.
|
|
651
|
-
this.encryptionVariant = config.encryptionVariant;
|
|
664
|
+
this.cipher = config.cipher;
|
|
652
665
|
this.logger = config.logger ?? ((msg, data) => logger.debug(msg, data));
|
|
653
666
|
}
|
|
654
667
|
/**
|
|
@@ -662,45 +675,45 @@ class RpcHandlerManager {
|
|
|
662
675
|
}
|
|
663
676
|
}
|
|
664
677
|
/**
|
|
665
|
-
*
|
|
678
|
+
* Route a decrypted RPC call to its handler. This is the plaintext core of
|
|
679
|
+
* the manager: it knows nothing about the wire (no base64, no cipher), so it
|
|
680
|
+
* is TOTAL — an unknown method or a throwing handler both resolve to an
|
|
681
|
+
* `{ error }` value rather than rejecting. That makes it the test surface for
|
|
682
|
+
* routing behaviour, exercised without any crypto setup.
|
|
666
683
|
*/
|
|
667
|
-
async
|
|
684
|
+
async dispatch(method, params) {
|
|
685
|
+
const handler = this.handlers.get(method);
|
|
686
|
+
if (!handler) {
|
|
687
|
+
this.logger("[RPC] [ERROR] Method not found", { method });
|
|
688
|
+
return { error: "Method not found" };
|
|
689
|
+
}
|
|
668
690
|
try {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
this.logger("[RPC] [ERROR] Method not found", {
|
|
672
|
-
method: request.method
|
|
673
|
-
});
|
|
674
|
-
const errorResponse = { error: "Method not found" };
|
|
675
|
-
return encodeBase64(
|
|
676
|
-
encrypt(this.encryptionKey, this.encryptionVariant, errorResponse)
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
const decryptedParams = decrypt(
|
|
680
|
-
this.encryptionKey,
|
|
681
|
-
this.encryptionVariant,
|
|
682
|
-
decodeBase64(request.params)
|
|
683
|
-
);
|
|
684
|
-
this.logger("[RPC] Calling handler", { method: request.method });
|
|
685
|
-
const result = await handler(decryptedParams);
|
|
691
|
+
this.logger("[RPC] Calling handler", { method });
|
|
692
|
+
const result = await handler(params);
|
|
686
693
|
this.logger("[RPC] Handler returned", {
|
|
687
|
-
method
|
|
694
|
+
method,
|
|
688
695
|
hasResult: result !== void 0
|
|
689
696
|
});
|
|
690
|
-
|
|
691
|
-
encrypt(this.encryptionKey, this.encryptionVariant, result)
|
|
692
|
-
);
|
|
693
|
-
return encryptedResponse;
|
|
697
|
+
return result;
|
|
694
698
|
} catch (error) {
|
|
695
699
|
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
696
|
-
|
|
697
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
698
|
-
};
|
|
699
|
-
return encodeBase64(
|
|
700
|
-
encrypt(this.encryptionKey, this.encryptionVariant, errorResponse)
|
|
701
|
-
);
|
|
700
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
702
701
|
}
|
|
703
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Handle an incoming wire RPC request: decrypt params, dispatch in plaintext,
|
|
705
|
+
* encrypt the result. The Cipher is the only encryption seam; on a decrypt
|
|
706
|
+
* failure the handler is still dispatched with `null` params (preserving the
|
|
707
|
+
* previous behaviour where a corrupt payload decrypted to `null`).
|
|
708
|
+
*/
|
|
709
|
+
async handleRequest(request) {
|
|
710
|
+
const decrypted = this.cipher.decrypt(request.params);
|
|
711
|
+
const result = await this.dispatch(
|
|
712
|
+
request.method,
|
|
713
|
+
decrypted.ok ? decrypted.value : null
|
|
714
|
+
);
|
|
715
|
+
return this.cipher.encrypt(result);
|
|
716
|
+
}
|
|
704
717
|
onSocketConnect(socket) {
|
|
705
718
|
this.socket = socket;
|
|
706
719
|
this.registerAllHandlers(socket);
|
|
@@ -1464,8 +1477,7 @@ class SessionClient extends node_events.EventEmitter {
|
|
|
1464
1477
|
});
|
|
1465
1478
|
this.rpcHandlerManager = createRpcHandlerManager({
|
|
1466
1479
|
scopePrefix: `session:${opts.sessionId}`,
|
|
1467
|
-
|
|
1468
|
-
encryptionVariant: opts.encryptionVariant,
|
|
1480
|
+
cipher: createCipher(opts.encryptionKey, opts.encryptionVariant),
|
|
1469
1481
|
logger: (msg, data) => logger.debug(msg, data)
|
|
1470
1482
|
});
|
|
1471
1483
|
if (opts.enableRpc !== false) {
|
|
@@ -2673,6 +2685,17 @@ const sessionEnvelopeSchema = z__namespace.object({
|
|
|
2673
2685
|
subagent: z__namespace.string().refine((value) => cuid2Exports.isCuid(value), {
|
|
2674
2686
|
message: "subagent must be a cuid2 value"
|
|
2675
2687
|
}).optional(),
|
|
2688
|
+
/**
|
|
2689
|
+
* Optional Claude-side message UUID for this envelope. Populated by the
|
|
2690
|
+
* CLI when an envelope mirrors a specific JSONL record, so the App can
|
|
2691
|
+
* use it as a precise rewind/fork anchor (the CLI's `forkSession` RPC
|
|
2692
|
+
* accepts this value as `upToMessageId`).
|
|
2693
|
+
*
|
|
2694
|
+
* Backward-compatible: older CLIs / non-Claude agents simply omit it.
|
|
2695
|
+
* App code MUST treat absence as "no fork anchor available" rather than
|
|
2696
|
+
* an error.
|
|
2697
|
+
*/
|
|
2698
|
+
claudeUuid: z__namespace.string().min(1).optional(),
|
|
2676
2699
|
ev: sessionEventSchema
|
|
2677
2700
|
}).superRefine((envelope, ctx) => {
|
|
2678
2701
|
if (envelope.ev.t === "service" && envelope.role !== "agent") {
|
|
@@ -3850,8 +3873,27 @@ const HAPPY_MCP_TOOL_NAMES = [
|
|
|
3850
3873
|
"change_title",
|
|
3851
3874
|
"query_project_knowledge",
|
|
3852
3875
|
"update_progress",
|
|
3853
|
-
"update_session_summary"
|
|
3876
|
+
"update_session_summary",
|
|
3877
|
+
"ask_user"
|
|
3854
3878
|
];
|
|
3879
|
+
const parseIfJsonString = (v) => {
|
|
3880
|
+
if (typeof v !== "string") return v;
|
|
3881
|
+
try {
|
|
3882
|
+
return JSON.parse(v);
|
|
3883
|
+
} catch {
|
|
3884
|
+
return v;
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
const looseStringArray = () => z.z.union([z.z.array(z.z.string()), z.z.string()]).transform((v) => {
|
|
3888
|
+
if (Array.isArray(v)) return v;
|
|
3889
|
+
const parsed = parseIfJsonString(v);
|
|
3890
|
+
if (Array.isArray(parsed)) return parsed.map(String);
|
|
3891
|
+
return v.length ? [v] : [];
|
|
3892
|
+
});
|
|
3893
|
+
const looseObjectArray = (item) => z.z.union([
|
|
3894
|
+
z.z.array(item),
|
|
3895
|
+
z.z.string().transform(parseIfJsonString).pipe(z.z.array(item))
|
|
3896
|
+
]);
|
|
3855
3897
|
const HAPPY_MCP_TOOL_SPECS = {
|
|
3856
3898
|
change_title: {
|
|
3857
3899
|
title: "Change Title",
|
|
@@ -3886,7 +3928,7 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3886
3928
|
description: 'Optional override for the App\'s Progress tab. In most cases your TodoWrite calls are auto-mirrored, so you do NOT need to call this. Use it only when you want to set extra fields the CLI hook does not capture (currentStage, blockers) or to force a new list boundary with `listId: "new"`.',
|
|
3887
3929
|
failureLabel: "Failed to update progress",
|
|
3888
3930
|
inputSchema: {
|
|
3889
|
-
todos:
|
|
3931
|
+
todos: looseObjectArray(
|
|
3890
3932
|
z.z.object({
|
|
3891
3933
|
content: z.z.string().describe("Concise description of the task"),
|
|
3892
3934
|
status: z.z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
|
|
@@ -3897,7 +3939,7 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3897
3939
|
})
|
|
3898
3940
|
).describe("The full checklist \u2014 always send every item, not a delta"),
|
|
3899
3941
|
currentStage: z.z.string().optional().describe("Optional overall stage name for the checklist"),
|
|
3900
|
-
blockers:
|
|
3942
|
+
blockers: looseStringArray().optional().describe("Optional list of things blocking progress"),
|
|
3901
3943
|
listId: z.z.string().optional().describe("Target list id. Use 'new' to force a fresh list"),
|
|
3902
3944
|
label: z.z.string().optional().describe("Short human-readable name for this task list")
|
|
3903
3945
|
},
|
|
@@ -3908,6 +3950,33 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3908
3950
|
fallbackAction: "Update progress",
|
|
3909
3951
|
reasonPhrases: ["progress update", "progress updates", "update_progress"]
|
|
3910
3952
|
},
|
|
3953
|
+
ask_user: {
|
|
3954
|
+
title: "Ask User",
|
|
3955
|
+
description: "Ask the user one or more questions via the App's native picker UI. Use this whenever AskUserQuestion is unavailable (the host has disabled it \u2014 common when running under happy-cli's PTY-mode Claude TUI). The input schema is identical to AskUserQuestion's. The tool blocks until the user submits answers in the App, then returns them as a JSON string keyed by question text.",
|
|
3956
|
+
failureLabel: "Failed to get user answer",
|
|
3957
|
+
inputSchema: {
|
|
3958
|
+
questions: looseObjectArray(
|
|
3959
|
+
z.z.object({
|
|
3960
|
+
question: z.z.string().describe("The question to ask the user"),
|
|
3961
|
+
header: z.z.string().describe("Short label/chip for the question (max ~12 chars)"),
|
|
3962
|
+
options: looseObjectArray(
|
|
3963
|
+
z.z.object({
|
|
3964
|
+
label: z.z.string().describe("Option label"),
|
|
3965
|
+
description: z.z.string().describe("Option description"),
|
|
3966
|
+
preview: z.z.string().optional().describe("Optional preview content shown when the option is focused")
|
|
3967
|
+
})
|
|
3968
|
+
).describe("Available choices for this question (2-4 options)"),
|
|
3969
|
+
multiSelect: z.z.boolean().describe("Allow multiple selections instead of single-select")
|
|
3970
|
+
})
|
|
3971
|
+
).describe("Questions to ask the user (1-4 questions)")
|
|
3972
|
+
},
|
|
3973
|
+
hideSuccessfulCall: false,
|
|
3974
|
+
autoApproveByDefault: false,
|
|
3975
|
+
permissionAction: "Waiting for user to answer",
|
|
3976
|
+
dynamicAction: "Waiting for user to answer",
|
|
3977
|
+
fallbackAction: "Ask user",
|
|
3978
|
+
reasonPhrases: ["ask user", "user input", "ask_user"]
|
|
3979
|
+
},
|
|
3911
3980
|
update_session_summary: {
|
|
3912
3981
|
title: "Update Session Summary",
|
|
3913
3982
|
description: "Write a narrative session summary the App shows above the progress checklist. Call at milestones, not per task: after first understanding the goal, when scope shifts significantly, when key decisions are made, or when moving to a new phase. Full rewrite each call.",
|
|
@@ -3915,9 +3984,9 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3915
3984
|
inputSchema: {
|
|
3916
3985
|
goal: z.z.string().describe("What the user ultimately wants to accomplish"),
|
|
3917
3986
|
currentFocus: z.z.string().optional().describe("Brief description of the active task or phase"),
|
|
3918
|
-
keyDecisions:
|
|
3919
|
-
openQuestions:
|
|
3920
|
-
impactScope:
|
|
3987
|
+
keyDecisions: looseStringArray().optional().describe("Important choices already made this session"),
|
|
3988
|
+
openQuestions: looseStringArray().optional().describe("Unresolved questions or pending decisions"),
|
|
3989
|
+
impactScope: looseStringArray().optional().describe("Modules/files/areas affected by this session's work"),
|
|
3921
3990
|
requestId: z.z.string().optional().describe(
|
|
3922
3991
|
"Optional request identifier that runtimes may record in sessionSummaryRefresh recent history for request-level confirmation"
|
|
3923
3992
|
)
|
|
@@ -3941,6 +4010,19 @@ HAPPY_MCP_TOOL_NAMES.filter(
|
|
|
3941
4010
|
HAPPY_MCP_TOOL_NAMES.filter(
|
|
3942
4011
|
(toolName) => HAPPY_MCP_TOOL_SPECS[toolName].hideSuccessfulCall
|
|
3943
4012
|
);
|
|
4013
|
+
z.z.object({
|
|
4014
|
+
askId: z.z.string(),
|
|
4015
|
+
answers: z.z.record(z.z.string(), z.z.string()),
|
|
4016
|
+
// When true the user explicitly declined to answer (tapped the "取消选择"
|
|
4017
|
+
// / "Decline" button in the App's picker). The happy-cli handler treats
|
|
4018
|
+
// this as an error so the MCP tool returns isError to Claude TUI — letting
|
|
4019
|
+
// the model know it did not get an answer and should pick a fallback path
|
|
4020
|
+
// (e.g. proceed with assumptions or ask differently).
|
|
4021
|
+
canceled: z.z.boolean().optional()
|
|
4022
|
+
}).strict();
|
|
4023
|
+
z.z.object({
|
|
4024
|
+
ok: z.z.literal(true)
|
|
4025
|
+
}).strict();
|
|
3944
4026
|
|
|
3945
4027
|
const CodexRuntimeConfigSchema = z__namespace.object({
|
|
3946
4028
|
model: z__namespace.string().nullish(),
|
|
@@ -4080,11 +4162,13 @@ z.z.object({
|
|
|
4080
4162
|
"invalid_arguments",
|
|
4081
4163
|
"permission_denied",
|
|
4082
4164
|
/**
|
|
4083
|
-
*
|
|
4084
|
-
*
|
|
4085
|
-
*
|
|
4086
|
-
*
|
|
4087
|
-
*
|
|
4165
|
+
* The agent runtime has no programmatic MCP-tool invocation surface.
|
|
4166
|
+
* In PTY mode the Claude TUI owns the MCP connections itself; the
|
|
4167
|
+
* historical Claude Agent SDK also never exposed a runtime
|
|
4168
|
+
* `callMcpTool()` on `Query`. The CLI handler returns this code so
|
|
4169
|
+
* the App can surface an honest "not supported" state instead of
|
|
4170
|
+
* masking the gap as a server error. Code name preserved for wire
|
|
4171
|
+
* compatibility across older CLI / App builds.
|
|
4088
4172
|
*/
|
|
4089
4173
|
"sdk_not_implemented",
|
|
4090
4174
|
"unknown"
|
|
@@ -4427,6 +4511,21 @@ z.z.object({
|
|
|
4427
4511
|
servers: z.z.record(z.z.string(), McpRegistryEntrySchema)
|
|
4428
4512
|
});
|
|
4429
4513
|
|
|
4514
|
+
z__namespace.discriminatedUnion("type", [
|
|
4515
|
+
z__namespace.object({
|
|
4516
|
+
type: z__namespace.literal("success"),
|
|
4517
|
+
sessionId: z__namespace.string()
|
|
4518
|
+
}),
|
|
4519
|
+
z__namespace.object({
|
|
4520
|
+
type: z__namespace.literal("requestToApproveDirectoryCreation"),
|
|
4521
|
+
directory: z__namespace.string()
|
|
4522
|
+
}),
|
|
4523
|
+
z__namespace.object({
|
|
4524
|
+
type: z__namespace.literal("error"),
|
|
4525
|
+
errorMessage: z__namespace.string()
|
|
4526
|
+
})
|
|
4527
|
+
]);
|
|
4528
|
+
|
|
4430
4529
|
const NOT_INSTALLED = Object.freeze({ status: "not-installed" });
|
|
4431
4530
|
const DISCONNECTED = Object.freeze({ status: "disconnected" });
|
|
4432
4531
|
const DETECT_TIMEOUT_MS = 3e3;
|
|
@@ -4902,8 +5001,7 @@ class MachineClient {
|
|
|
4902
5001
|
this.onEphemeral = opts.onEphemeral;
|
|
4903
5002
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
4904
5003
|
scopePrefix: opts.machine.id,
|
|
4905
|
-
|
|
4906
|
-
encryptionVariant: opts.machine.encryptionVariant,
|
|
5004
|
+
cipher: createCipher(opts.machine.encryptionKey, opts.machine.encryptionVariant),
|
|
4907
5005
|
logger: (msg, data) => logger.debug(msg, data)
|
|
4908
5006
|
});
|
|
4909
5007
|
const workDir = opts.workingDirectory ?? process.cwd();
|
package/dist/index.d.cts
CHANGED
|
@@ -529,10 +529,46 @@ declare function getOrCreateMachine(config: Config, creds: Credentials, metadata
|
|
|
529
529
|
*/
|
|
530
530
|
declare function listMachines(config: Config, creds: Credentials): Promise<RawMachine[]>;
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Outcome of a decrypt attempt.
|
|
534
|
+
*
|
|
535
|
+
* The low-level {@link decrypt} returns `unknown | null`, fusing "could not
|
|
536
|
+
* decrypt" (wrong key, tampered bytes, wrong variant, non-JSON plaintext)
|
|
537
|
+
* with a value that legitimately decrypted to something falsy.
|
|
538
|
+
* `DecryptResult` splits those apart: `{ ok: false }` is an authentication or
|
|
539
|
+
* parse failure, while `{ ok: true, value }` carries the recovered plaintext.
|
|
540
|
+
* Callers branch on `ok` instead of guessing from a collapsed `null`.
|
|
541
|
+
*/
|
|
542
|
+
type DecryptResult = {
|
|
543
|
+
ok: true;
|
|
544
|
+
value: any;
|
|
545
|
+
} | {
|
|
546
|
+
ok: false;
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* A Cipher binds one AccessKey + encryption variant into a small interface.
|
|
550
|
+
*
|
|
551
|
+
* It is the single seam every transport client encrypts/decrypts through:
|
|
552
|
+
* hand it a value and get a wire-ready base64 string, or hand it a base64
|
|
553
|
+
* wire string and get a {@link DecryptResult}. The variant choice (legacy
|
|
554
|
+
* NaCl secretbox vs AES-256-GCM dataKey) and the base64 framing live behind
|
|
555
|
+
* this interface, so call sites never thread `(key, variant)` or call
|
|
556
|
+
* `encode`/`decodeBase64` themselves — a wrong-variant or wrong-key bug can
|
|
557
|
+
* only originate at the one `createCipher` call, not at the dozens of places
|
|
558
|
+
* that used to repeat the pattern.
|
|
559
|
+
*/
|
|
560
|
+
interface Cipher {
|
|
561
|
+
/** Encrypt a JSON-serializable value, returning a base64 wire string. */
|
|
562
|
+
encrypt(data: any): string;
|
|
563
|
+
/** Decode + decrypt a base64 wire string. Never throws. */
|
|
564
|
+
decrypt(data: string): DecryptResult;
|
|
565
|
+
}
|
|
566
|
+
|
|
532
567
|
/**
|
|
533
568
|
* Common RPC types and interfaces for both session and machine clients.
|
|
534
569
|
* Mirrors happy-cli/src/api/rpc/types.ts
|
|
535
570
|
*/
|
|
571
|
+
|
|
536
572
|
/**
|
|
537
573
|
* Generic RPC handler function type
|
|
538
574
|
*/
|
|
@@ -549,8 +585,7 @@ interface RpcRequest {
|
|
|
549
585
|
*/
|
|
550
586
|
interface RpcHandlerConfig {
|
|
551
587
|
scopePrefix: string;
|
|
552
|
-
|
|
553
|
-
encryptionVariant: "legacy" | "dataKey";
|
|
588
|
+
cipher: Cipher;
|
|
554
589
|
logger?: (message: string, data?: unknown) => void;
|
|
555
590
|
}
|
|
556
591
|
|
|
@@ -565,8 +600,7 @@ interface RpcHandlerConfig {
|
|
|
565
600
|
declare class RpcHandlerManager {
|
|
566
601
|
private handlers;
|
|
567
602
|
private readonly scopePrefix;
|
|
568
|
-
private readonly
|
|
569
|
-
private readonly encryptionVariant;
|
|
603
|
+
private readonly cipher;
|
|
570
604
|
private readonly logger;
|
|
571
605
|
private socket;
|
|
572
606
|
private reregisterInterval;
|
|
@@ -577,7 +611,18 @@ declare class RpcHandlerManager {
|
|
|
577
611
|
*/
|
|
578
612
|
registerHandler<TRequest = any, TResponse = any>(method: string, handler: RpcHandler<TRequest, TResponse>): void;
|
|
579
613
|
/**
|
|
580
|
-
*
|
|
614
|
+
* Route a decrypted RPC call to its handler. This is the plaintext core of
|
|
615
|
+
* the manager: it knows nothing about the wire (no base64, no cipher), so it
|
|
616
|
+
* is TOTAL — an unknown method or a throwing handler both resolve to an
|
|
617
|
+
* `{ error }` value rather than rejecting. That makes it the test surface for
|
|
618
|
+
* routing behaviour, exercised without any crypto setup.
|
|
619
|
+
*/
|
|
620
|
+
dispatch(method: string, params: unknown): Promise<unknown>;
|
|
621
|
+
/**
|
|
622
|
+
* Handle an incoming wire RPC request: decrypt params, dispatch in plaintext,
|
|
623
|
+
* encrypt the result. The Cipher is the only encryption seam; on a decrypt
|
|
624
|
+
* failure the handler is still dispatched with `null` params (preserving the
|
|
625
|
+
* previous behaviour where a corrupt payload decrypted to `null`).
|
|
581
626
|
*/
|
|
582
627
|
handleRequest(request: RpcRequest): Promise<string>;
|
|
583
628
|
onSocketConnect(socket: Socket): void;
|
package/dist/index.d.mts
CHANGED
|
@@ -529,10 +529,46 @@ declare function getOrCreateMachine(config: Config, creds: Credentials, metadata
|
|
|
529
529
|
*/
|
|
530
530
|
declare function listMachines(config: Config, creds: Credentials): Promise<RawMachine[]>;
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Outcome of a decrypt attempt.
|
|
534
|
+
*
|
|
535
|
+
* The low-level {@link decrypt} returns `unknown | null`, fusing "could not
|
|
536
|
+
* decrypt" (wrong key, tampered bytes, wrong variant, non-JSON plaintext)
|
|
537
|
+
* with a value that legitimately decrypted to something falsy.
|
|
538
|
+
* `DecryptResult` splits those apart: `{ ok: false }` is an authentication or
|
|
539
|
+
* parse failure, while `{ ok: true, value }` carries the recovered plaintext.
|
|
540
|
+
* Callers branch on `ok` instead of guessing from a collapsed `null`.
|
|
541
|
+
*/
|
|
542
|
+
type DecryptResult = {
|
|
543
|
+
ok: true;
|
|
544
|
+
value: any;
|
|
545
|
+
} | {
|
|
546
|
+
ok: false;
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* A Cipher binds one AccessKey + encryption variant into a small interface.
|
|
550
|
+
*
|
|
551
|
+
* It is the single seam every transport client encrypts/decrypts through:
|
|
552
|
+
* hand it a value and get a wire-ready base64 string, or hand it a base64
|
|
553
|
+
* wire string and get a {@link DecryptResult}. The variant choice (legacy
|
|
554
|
+
* NaCl secretbox vs AES-256-GCM dataKey) and the base64 framing live behind
|
|
555
|
+
* this interface, so call sites never thread `(key, variant)` or call
|
|
556
|
+
* `encode`/`decodeBase64` themselves — a wrong-variant or wrong-key bug can
|
|
557
|
+
* only originate at the one `createCipher` call, not at the dozens of places
|
|
558
|
+
* that used to repeat the pattern.
|
|
559
|
+
*/
|
|
560
|
+
interface Cipher {
|
|
561
|
+
/** Encrypt a JSON-serializable value, returning a base64 wire string. */
|
|
562
|
+
encrypt(data: any): string;
|
|
563
|
+
/** Decode + decrypt a base64 wire string. Never throws. */
|
|
564
|
+
decrypt(data: string): DecryptResult;
|
|
565
|
+
}
|
|
566
|
+
|
|
532
567
|
/**
|
|
533
568
|
* Common RPC types and interfaces for both session and machine clients.
|
|
534
569
|
* Mirrors happy-cli/src/api/rpc/types.ts
|
|
535
570
|
*/
|
|
571
|
+
|
|
536
572
|
/**
|
|
537
573
|
* Generic RPC handler function type
|
|
538
574
|
*/
|
|
@@ -549,8 +585,7 @@ interface RpcRequest {
|
|
|
549
585
|
*/
|
|
550
586
|
interface RpcHandlerConfig {
|
|
551
587
|
scopePrefix: string;
|
|
552
|
-
|
|
553
|
-
encryptionVariant: "legacy" | "dataKey";
|
|
588
|
+
cipher: Cipher;
|
|
554
589
|
logger?: (message: string, data?: unknown) => void;
|
|
555
590
|
}
|
|
556
591
|
|
|
@@ -565,8 +600,7 @@ interface RpcHandlerConfig {
|
|
|
565
600
|
declare class RpcHandlerManager {
|
|
566
601
|
private handlers;
|
|
567
602
|
private readonly scopePrefix;
|
|
568
|
-
private readonly
|
|
569
|
-
private readonly encryptionVariant;
|
|
603
|
+
private readonly cipher;
|
|
570
604
|
private readonly logger;
|
|
571
605
|
private socket;
|
|
572
606
|
private reregisterInterval;
|
|
@@ -577,7 +611,18 @@ declare class RpcHandlerManager {
|
|
|
577
611
|
*/
|
|
578
612
|
registerHandler<TRequest = any, TResponse = any>(method: string, handler: RpcHandler<TRequest, TResponse>): void;
|
|
579
613
|
/**
|
|
580
|
-
*
|
|
614
|
+
* Route a decrypted RPC call to its handler. This is the plaintext core of
|
|
615
|
+
* the manager: it knows nothing about the wire (no base64, no cipher), so it
|
|
616
|
+
* is TOTAL — an unknown method or a throwing handler both resolve to an
|
|
617
|
+
* `{ error }` value rather than rejecting. That makes it the test surface for
|
|
618
|
+
* routing behaviour, exercised without any crypto setup.
|
|
619
|
+
*/
|
|
620
|
+
dispatch(method: string, params: unknown): Promise<unknown>;
|
|
621
|
+
/**
|
|
622
|
+
* Handle an incoming wire RPC request: decrypt params, dispatch in plaintext,
|
|
623
|
+
* encrypt the result. The Cipher is the only encryption seam; on a decrypt
|
|
624
|
+
* failure the handler is still dispatched with `null` params (preserving the
|
|
625
|
+
* previous behaviour where a corrupt payload decrypted to `null`).
|
|
581
626
|
*/
|
|
582
627
|
handleRequest(request: RpcRequest): Promise<string>;
|
|
583
628
|
onSocketConnect(socket: Socket): void;
|
package/dist/index.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import * as z from 'zod';
|
|
|
19
19
|
import { z as z$1 } from 'zod';
|
|
20
20
|
import { createServer } from 'http';
|
|
21
21
|
|
|
22
|
-
var version = "0.7.
|
|
22
|
+
var version = "0.7.2";
|
|
23
23
|
|
|
24
24
|
function loadConfig() {
|
|
25
25
|
const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://s.sangreal.code.xycloud.info:2443").replace(/\/+$/, "");
|
|
@@ -137,6 +137,21 @@ function decrypt(key, variant, data) {
|
|
|
137
137
|
return decryptWithDataKey(data, key);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
function createCipher(key, variant) {
|
|
141
|
+
return {
|
|
142
|
+
encrypt(data) {
|
|
143
|
+
return encodeBase64(encrypt(key, variant, data));
|
|
144
|
+
},
|
|
145
|
+
decrypt(data) {
|
|
146
|
+
try {
|
|
147
|
+
const value = decrypt(key, variant, decodeBase64(data));
|
|
148
|
+
return value === null ? { ok: false } : { ok: true, value };
|
|
149
|
+
} catch {
|
|
150
|
+
return { ok: false };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
140
155
|
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
|
|
141
156
|
const ephemeralKeyPair = tweetnacl.box.keyPair();
|
|
142
157
|
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
|
|
@@ -619,16 +634,14 @@ async function withBackoff(fn, options) {
|
|
|
619
634
|
class RpcHandlerManager {
|
|
620
635
|
handlers = /* @__PURE__ */ new Map();
|
|
621
636
|
scopePrefix;
|
|
622
|
-
|
|
623
|
-
encryptionVariant;
|
|
637
|
+
cipher;
|
|
624
638
|
logger;
|
|
625
639
|
socket = null;
|
|
626
640
|
reregisterInterval = null;
|
|
627
641
|
fastRetryTimer = null;
|
|
628
642
|
constructor(config) {
|
|
629
643
|
this.scopePrefix = config.scopePrefix;
|
|
630
|
-
this.
|
|
631
|
-
this.encryptionVariant = config.encryptionVariant;
|
|
644
|
+
this.cipher = config.cipher;
|
|
632
645
|
this.logger = config.logger ?? ((msg, data) => logger.debug(msg, data));
|
|
633
646
|
}
|
|
634
647
|
/**
|
|
@@ -642,45 +655,45 @@ class RpcHandlerManager {
|
|
|
642
655
|
}
|
|
643
656
|
}
|
|
644
657
|
/**
|
|
645
|
-
*
|
|
658
|
+
* Route a decrypted RPC call to its handler. This is the plaintext core of
|
|
659
|
+
* the manager: it knows nothing about the wire (no base64, no cipher), so it
|
|
660
|
+
* is TOTAL — an unknown method or a throwing handler both resolve to an
|
|
661
|
+
* `{ error }` value rather than rejecting. That makes it the test surface for
|
|
662
|
+
* routing behaviour, exercised without any crypto setup.
|
|
646
663
|
*/
|
|
647
|
-
async
|
|
664
|
+
async dispatch(method, params) {
|
|
665
|
+
const handler = this.handlers.get(method);
|
|
666
|
+
if (!handler) {
|
|
667
|
+
this.logger("[RPC] [ERROR] Method not found", { method });
|
|
668
|
+
return { error: "Method not found" };
|
|
669
|
+
}
|
|
648
670
|
try {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
this.logger("[RPC] [ERROR] Method not found", {
|
|
652
|
-
method: request.method
|
|
653
|
-
});
|
|
654
|
-
const errorResponse = { error: "Method not found" };
|
|
655
|
-
return encodeBase64(
|
|
656
|
-
encrypt(this.encryptionKey, this.encryptionVariant, errorResponse)
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
const decryptedParams = decrypt(
|
|
660
|
-
this.encryptionKey,
|
|
661
|
-
this.encryptionVariant,
|
|
662
|
-
decodeBase64(request.params)
|
|
663
|
-
);
|
|
664
|
-
this.logger("[RPC] Calling handler", { method: request.method });
|
|
665
|
-
const result = await handler(decryptedParams);
|
|
671
|
+
this.logger("[RPC] Calling handler", { method });
|
|
672
|
+
const result = await handler(params);
|
|
666
673
|
this.logger("[RPC] Handler returned", {
|
|
667
|
-
method
|
|
674
|
+
method,
|
|
668
675
|
hasResult: result !== void 0
|
|
669
676
|
});
|
|
670
|
-
|
|
671
|
-
encrypt(this.encryptionKey, this.encryptionVariant, result)
|
|
672
|
-
);
|
|
673
|
-
return encryptedResponse;
|
|
677
|
+
return result;
|
|
674
678
|
} catch (error) {
|
|
675
679
|
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
676
|
-
|
|
677
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
678
|
-
};
|
|
679
|
-
return encodeBase64(
|
|
680
|
-
encrypt(this.encryptionKey, this.encryptionVariant, errorResponse)
|
|
681
|
-
);
|
|
680
|
+
return { error: error instanceof Error ? error.message : "Unknown error" };
|
|
682
681
|
}
|
|
683
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Handle an incoming wire RPC request: decrypt params, dispatch in plaintext,
|
|
685
|
+
* encrypt the result. The Cipher is the only encryption seam; on a decrypt
|
|
686
|
+
* failure the handler is still dispatched with `null` params (preserving the
|
|
687
|
+
* previous behaviour where a corrupt payload decrypted to `null`).
|
|
688
|
+
*/
|
|
689
|
+
async handleRequest(request) {
|
|
690
|
+
const decrypted = this.cipher.decrypt(request.params);
|
|
691
|
+
const result = await this.dispatch(
|
|
692
|
+
request.method,
|
|
693
|
+
decrypted.ok ? decrypted.value : null
|
|
694
|
+
);
|
|
695
|
+
return this.cipher.encrypt(result);
|
|
696
|
+
}
|
|
684
697
|
onSocketConnect(socket) {
|
|
685
698
|
this.socket = socket;
|
|
686
699
|
this.registerAllHandlers(socket);
|
|
@@ -1444,8 +1457,7 @@ class SessionClient extends EventEmitter {
|
|
|
1444
1457
|
});
|
|
1445
1458
|
this.rpcHandlerManager = createRpcHandlerManager({
|
|
1446
1459
|
scopePrefix: `session:${opts.sessionId}`,
|
|
1447
|
-
|
|
1448
|
-
encryptionVariant: opts.encryptionVariant,
|
|
1460
|
+
cipher: createCipher(opts.encryptionKey, opts.encryptionVariant),
|
|
1449
1461
|
logger: (msg, data) => logger.debug(msg, data)
|
|
1450
1462
|
});
|
|
1451
1463
|
if (opts.enableRpc !== false) {
|
|
@@ -2653,6 +2665,17 @@ const sessionEnvelopeSchema = z.object({
|
|
|
2653
2665
|
subagent: z.string().refine((value) => cuid2Exports.isCuid(value), {
|
|
2654
2666
|
message: "subagent must be a cuid2 value"
|
|
2655
2667
|
}).optional(),
|
|
2668
|
+
/**
|
|
2669
|
+
* Optional Claude-side message UUID for this envelope. Populated by the
|
|
2670
|
+
* CLI when an envelope mirrors a specific JSONL record, so the App can
|
|
2671
|
+
* use it as a precise rewind/fork anchor (the CLI's `forkSession` RPC
|
|
2672
|
+
* accepts this value as `upToMessageId`).
|
|
2673
|
+
*
|
|
2674
|
+
* Backward-compatible: older CLIs / non-Claude agents simply omit it.
|
|
2675
|
+
* App code MUST treat absence as "no fork anchor available" rather than
|
|
2676
|
+
* an error.
|
|
2677
|
+
*/
|
|
2678
|
+
claudeUuid: z.string().min(1).optional(),
|
|
2656
2679
|
ev: sessionEventSchema
|
|
2657
2680
|
}).superRefine((envelope, ctx) => {
|
|
2658
2681
|
if (envelope.ev.t === "service" && envelope.role !== "agent") {
|
|
@@ -3830,8 +3853,27 @@ const HAPPY_MCP_TOOL_NAMES = [
|
|
|
3830
3853
|
"change_title",
|
|
3831
3854
|
"query_project_knowledge",
|
|
3832
3855
|
"update_progress",
|
|
3833
|
-
"update_session_summary"
|
|
3856
|
+
"update_session_summary",
|
|
3857
|
+
"ask_user"
|
|
3834
3858
|
];
|
|
3859
|
+
const parseIfJsonString = (v) => {
|
|
3860
|
+
if (typeof v !== "string") return v;
|
|
3861
|
+
try {
|
|
3862
|
+
return JSON.parse(v);
|
|
3863
|
+
} catch {
|
|
3864
|
+
return v;
|
|
3865
|
+
}
|
|
3866
|
+
};
|
|
3867
|
+
const looseStringArray = () => z$1.union([z$1.array(z$1.string()), z$1.string()]).transform((v) => {
|
|
3868
|
+
if (Array.isArray(v)) return v;
|
|
3869
|
+
const parsed = parseIfJsonString(v);
|
|
3870
|
+
if (Array.isArray(parsed)) return parsed.map(String);
|
|
3871
|
+
return v.length ? [v] : [];
|
|
3872
|
+
});
|
|
3873
|
+
const looseObjectArray = (item) => z$1.union([
|
|
3874
|
+
z$1.array(item),
|
|
3875
|
+
z$1.string().transform(parseIfJsonString).pipe(z$1.array(item))
|
|
3876
|
+
]);
|
|
3835
3877
|
const HAPPY_MCP_TOOL_SPECS = {
|
|
3836
3878
|
change_title: {
|
|
3837
3879
|
title: "Change Title",
|
|
@@ -3866,7 +3908,7 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3866
3908
|
description: 'Optional override for the App\'s Progress tab. In most cases your TodoWrite calls are auto-mirrored, so you do NOT need to call this. Use it only when you want to set extra fields the CLI hook does not capture (currentStage, blockers) or to force a new list boundary with `listId: "new"`.',
|
|
3867
3909
|
failureLabel: "Failed to update progress",
|
|
3868
3910
|
inputSchema: {
|
|
3869
|
-
todos:
|
|
3911
|
+
todos: looseObjectArray(
|
|
3870
3912
|
z$1.object({
|
|
3871
3913
|
content: z$1.string().describe("Concise description of the task"),
|
|
3872
3914
|
status: z$1.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
|
|
@@ -3877,7 +3919,7 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3877
3919
|
})
|
|
3878
3920
|
).describe("The full checklist \u2014 always send every item, not a delta"),
|
|
3879
3921
|
currentStage: z$1.string().optional().describe("Optional overall stage name for the checklist"),
|
|
3880
|
-
blockers:
|
|
3922
|
+
blockers: looseStringArray().optional().describe("Optional list of things blocking progress"),
|
|
3881
3923
|
listId: z$1.string().optional().describe("Target list id. Use 'new' to force a fresh list"),
|
|
3882
3924
|
label: z$1.string().optional().describe("Short human-readable name for this task list")
|
|
3883
3925
|
},
|
|
@@ -3888,6 +3930,33 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3888
3930
|
fallbackAction: "Update progress",
|
|
3889
3931
|
reasonPhrases: ["progress update", "progress updates", "update_progress"]
|
|
3890
3932
|
},
|
|
3933
|
+
ask_user: {
|
|
3934
|
+
title: "Ask User",
|
|
3935
|
+
description: "Ask the user one or more questions via the App's native picker UI. Use this whenever AskUserQuestion is unavailable (the host has disabled it \u2014 common when running under happy-cli's PTY-mode Claude TUI). The input schema is identical to AskUserQuestion's. The tool blocks until the user submits answers in the App, then returns them as a JSON string keyed by question text.",
|
|
3936
|
+
failureLabel: "Failed to get user answer",
|
|
3937
|
+
inputSchema: {
|
|
3938
|
+
questions: looseObjectArray(
|
|
3939
|
+
z$1.object({
|
|
3940
|
+
question: z$1.string().describe("The question to ask the user"),
|
|
3941
|
+
header: z$1.string().describe("Short label/chip for the question (max ~12 chars)"),
|
|
3942
|
+
options: looseObjectArray(
|
|
3943
|
+
z$1.object({
|
|
3944
|
+
label: z$1.string().describe("Option label"),
|
|
3945
|
+
description: z$1.string().describe("Option description"),
|
|
3946
|
+
preview: z$1.string().optional().describe("Optional preview content shown when the option is focused")
|
|
3947
|
+
})
|
|
3948
|
+
).describe("Available choices for this question (2-4 options)"),
|
|
3949
|
+
multiSelect: z$1.boolean().describe("Allow multiple selections instead of single-select")
|
|
3950
|
+
})
|
|
3951
|
+
).describe("Questions to ask the user (1-4 questions)")
|
|
3952
|
+
},
|
|
3953
|
+
hideSuccessfulCall: false,
|
|
3954
|
+
autoApproveByDefault: false,
|
|
3955
|
+
permissionAction: "Waiting for user to answer",
|
|
3956
|
+
dynamicAction: "Waiting for user to answer",
|
|
3957
|
+
fallbackAction: "Ask user",
|
|
3958
|
+
reasonPhrases: ["ask user", "user input", "ask_user"]
|
|
3959
|
+
},
|
|
3891
3960
|
update_session_summary: {
|
|
3892
3961
|
title: "Update Session Summary",
|
|
3893
3962
|
description: "Write a narrative session summary the App shows above the progress checklist. Call at milestones, not per task: after first understanding the goal, when scope shifts significantly, when key decisions are made, or when moving to a new phase. Full rewrite each call.",
|
|
@@ -3895,9 +3964,9 @@ const HAPPY_MCP_TOOL_SPECS = {
|
|
|
3895
3964
|
inputSchema: {
|
|
3896
3965
|
goal: z$1.string().describe("What the user ultimately wants to accomplish"),
|
|
3897
3966
|
currentFocus: z$1.string().optional().describe("Brief description of the active task or phase"),
|
|
3898
|
-
keyDecisions:
|
|
3899
|
-
openQuestions:
|
|
3900
|
-
impactScope:
|
|
3967
|
+
keyDecisions: looseStringArray().optional().describe("Important choices already made this session"),
|
|
3968
|
+
openQuestions: looseStringArray().optional().describe("Unresolved questions or pending decisions"),
|
|
3969
|
+
impactScope: looseStringArray().optional().describe("Modules/files/areas affected by this session's work"),
|
|
3901
3970
|
requestId: z$1.string().optional().describe(
|
|
3902
3971
|
"Optional request identifier that runtimes may record in sessionSummaryRefresh recent history for request-level confirmation"
|
|
3903
3972
|
)
|
|
@@ -3921,6 +3990,19 @@ HAPPY_MCP_TOOL_NAMES.filter(
|
|
|
3921
3990
|
HAPPY_MCP_TOOL_NAMES.filter(
|
|
3922
3991
|
(toolName) => HAPPY_MCP_TOOL_SPECS[toolName].hideSuccessfulCall
|
|
3923
3992
|
);
|
|
3993
|
+
z$1.object({
|
|
3994
|
+
askId: z$1.string(),
|
|
3995
|
+
answers: z$1.record(z$1.string(), z$1.string()),
|
|
3996
|
+
// When true the user explicitly declined to answer (tapped the "取消选择"
|
|
3997
|
+
// / "Decline" button in the App's picker). The happy-cli handler treats
|
|
3998
|
+
// this as an error so the MCP tool returns isError to Claude TUI — letting
|
|
3999
|
+
// the model know it did not get an answer and should pick a fallback path
|
|
4000
|
+
// (e.g. proceed with assumptions or ask differently).
|
|
4001
|
+
canceled: z$1.boolean().optional()
|
|
4002
|
+
}).strict();
|
|
4003
|
+
z$1.object({
|
|
4004
|
+
ok: z$1.literal(true)
|
|
4005
|
+
}).strict();
|
|
3924
4006
|
|
|
3925
4007
|
const CodexRuntimeConfigSchema = z.object({
|
|
3926
4008
|
model: z.string().nullish(),
|
|
@@ -4060,11 +4142,13 @@ z$1.object({
|
|
|
4060
4142
|
"invalid_arguments",
|
|
4061
4143
|
"permission_denied",
|
|
4062
4144
|
/**
|
|
4063
|
-
*
|
|
4064
|
-
*
|
|
4065
|
-
*
|
|
4066
|
-
*
|
|
4067
|
-
*
|
|
4145
|
+
* The agent runtime has no programmatic MCP-tool invocation surface.
|
|
4146
|
+
* In PTY mode the Claude TUI owns the MCP connections itself; the
|
|
4147
|
+
* historical Claude Agent SDK also never exposed a runtime
|
|
4148
|
+
* `callMcpTool()` on `Query`. The CLI handler returns this code so
|
|
4149
|
+
* the App can surface an honest "not supported" state instead of
|
|
4150
|
+
* masking the gap as a server error. Code name preserved for wire
|
|
4151
|
+
* compatibility across older CLI / App builds.
|
|
4068
4152
|
*/
|
|
4069
4153
|
"sdk_not_implemented",
|
|
4070
4154
|
"unknown"
|
|
@@ -4407,6 +4491,21 @@ z$1.object({
|
|
|
4407
4491
|
servers: z$1.record(z$1.string(), McpRegistryEntrySchema)
|
|
4408
4492
|
});
|
|
4409
4493
|
|
|
4494
|
+
z.discriminatedUnion("type", [
|
|
4495
|
+
z.object({
|
|
4496
|
+
type: z.literal("success"),
|
|
4497
|
+
sessionId: z.string()
|
|
4498
|
+
}),
|
|
4499
|
+
z.object({
|
|
4500
|
+
type: z.literal("requestToApproveDirectoryCreation"),
|
|
4501
|
+
directory: z.string()
|
|
4502
|
+
}),
|
|
4503
|
+
z.object({
|
|
4504
|
+
type: z.literal("error"),
|
|
4505
|
+
errorMessage: z.string()
|
|
4506
|
+
})
|
|
4507
|
+
]);
|
|
4508
|
+
|
|
4410
4509
|
const NOT_INSTALLED = Object.freeze({ status: "not-installed" });
|
|
4411
4510
|
const DISCONNECTED = Object.freeze({ status: "disconnected" });
|
|
4412
4511
|
const DETECT_TIMEOUT_MS = 3e3;
|
|
@@ -4882,8 +4981,7 @@ class MachineClient {
|
|
|
4882
4981
|
this.onEphemeral = opts.onEphemeral;
|
|
4883
4982
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
4884
4983
|
scopePrefix: opts.machine.id,
|
|
4885
|
-
|
|
4886
|
-
encryptionVariant: opts.machine.encryptionVariant,
|
|
4984
|
+
cipher: createCipher(opts.machine.encryptionKey, opts.machine.encryptionVariant),
|
|
4887
4985
|
logger: (msg, data) => logger.debug(msg, data)
|
|
4888
4986
|
});
|
|
4889
4987
|
const workDir = opts.workingDirectory ?? process.cwd();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kmmao/happy-agent",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "CLI client for controlling Happy Coder agents remotely",
|
|
5
5
|
"author": "Kirill Dubovitskiy",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"release": "npx --no-install release-it"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@kmmao/happy-wire": "^0.
|
|
46
|
+
"@kmmao/happy-wire": "^0.24.0",
|
|
47
47
|
"axios": "^1.15.2",
|
|
48
48
|
"chalk": "^5.6.2",
|
|
49
49
|
"commander": "^14.0.0",
|