@zonease/happy 0.6.38 → 0.6.40
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/bin/happy.mjs +10 -0
- package/dist/{types-DvTsFaqR.cjs → api-C7FyJfZ1.cjs} +400 -1964
- package/dist/{types-BWpRU1VN.mjs → api-D6WL40-U.mjs} +220 -1729
- package/dist/auth-C7ZScWEw.cjs +274 -0
- package/dist/auth-DCsIoZLT.cjs +183 -0
- package/dist/auth-DO9GexAR.mjs +181 -0
- package/dist/auth-DxJ3VvrX.mjs +270 -0
- package/dist/browser-AoVGRX7y.mjs +20 -0
- package/dist/browser-DzgO4oaD.cjs +22 -0
- package/dist/claudeLocal-C6BdzhDx.cjs +352 -0
- package/dist/claudeLocal-CWNGpJ0-.mjs +344 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -1
- package/dist/codex/happyMcpStdioBridge.mjs +4 -1
- package/dist/common-BdNgghUJ.mjs +75 -0
- package/dist/common-CZvSRU4p.cjs +83 -0
- package/dist/{config-0Xnc8ARM.cjs → config-CclB3QW9.cjs} +21 -34
- package/dist/{config-_GW4YBCP.mjs → config-DZk-mJ0K.mjs} +7 -20
- package/dist/connect-BXLUKJ5P.mjs +679 -0
- package/dist/connect-k9CqDJ-T.cjs +681 -0
- package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
- package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
- package/dist/controlClient-B23M-WPp.cjs +177 -0
- package/dist/controlClient-DB5tDU7V.mjs +167 -0
- package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
- package/dist/deviceKeys-C540DcXl.cjs +182 -0
- package/dist/diagnose-B3D1a_v0.cjs +60 -0
- package/dist/diagnose-CuRKXxt1.mjs +58 -0
- package/dist/doctor-BQGLhrcI.cjs +214 -0
- package/dist/doctor-C08m5Z9f.cjs +78 -0
- package/dist/doctor-CU4JLGQ1.mjs +211 -0
- package/dist/doctor-ON7pN5G5.mjs +74 -0
- package/dist/downloadImage-DZXI3mXL.mjs +887 -0
- package/dist/downloadImage-cQ1Z7hkC.cjs +897 -0
- package/dist/enroll-B55BQR56.mjs +113 -0
- package/dist/enroll-Bo-CNio1.cjs +115 -0
- package/dist/evidence-DQp8QWcv.cjs +129 -0
- package/dist/evidence-DTYRAaOZ.mjs +127 -0
- package/dist/formatImageMessage-CixDYY6G.cjs +46 -0
- package/dist/formatImageMessage-DbKSSjMx.mjs +43 -0
- package/dist/github-DKcyF1zp.mjs +57 -0
- package/dist/github-h9GhJJ_C.cjs +59 -0
- package/dist/index-CDTNgfUf.cjs +578 -0
- package/dist/index-CPKdvi7J.mjs +554 -0
- package/dist/index.cjs +812 -40
- package/dist/index.mjs +813 -40
- package/dist/install-B1e6rdy7.mjs +94 -0
- package/dist/install-CgUPGIoF.mjs +95 -0
- package/dist/install-D7Ir7ciZ.cjs +96 -0
- package/dist/install-DSDufaPd.cjs +98 -0
- package/dist/lib.cjs +18 -12
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +13 -7
- package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Bfei9wPq.mjs} +4 -2
- package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-CbLGtvST.cjs} +63 -61
- package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
- package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
- package/dist/persistence-CBTtC_dm.cjs +996 -0
- package/dist/persistence-DKfF81pd.mjs +944 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/prompt-BTWDZZO0.cjs +25 -0
- package/dist/prompt-BsAZsPbo.mjs +22 -0
- package/dist/release-BMEzNONE.mjs +168 -0
- package/dist/release-EhOJeAZe.cjs +171 -0
- package/dist/run-BVZsMNwN.mjs +2164 -0
- package/dist/run-CURx7LhK.cjs +2188 -0
- package/dist/runClaude-6Gi0sYQy.mjs +3763 -0
- package/dist/runClaude-CL3Kt5ex.cjs +3766 -0
- package/dist/{runCodex-DiKzBf9q.mjs → runCodex-ClwEgJ7M.mjs} +36 -20
- package/dist/{runCodex-skq0j05d.cjs → runCodex-YKyVBkJ3.cjs} +169 -153
- package/dist/{runGemini-BeqrVq8R.mjs → runGemini-BPWxVr60.mjs} +34 -17
- package/dist/{runGemini-CVp-CEIh.cjs → runGemini-Xz2W1spn.cjs} +212 -195
- package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-CdCWiiLr.mjs} +14 -38
- package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-DjZyCvDI.cjs} +31 -55
- package/dist/{serve-BNdoEDVx.mjs → serve-B48JttQ3.mjs} +12 -35
- package/dist/{serve-BECdtivY.cjs → serve-B8vaWQA0.cjs} +35 -58
- package/dist/session-BXboDn2V.mjs +108 -0
- package/dist/session-BZO25zxp.cjs +110 -0
- package/dist/sessionKeyCache-B6cGadYL.mjs +33 -0
- package/dist/sessionKeyCache-CDmexJ9p.cjs +36 -0
- package/dist/sessionList-CgAYZpNj.mjs +51 -0
- package/dist/sessionList-lQ-nqEhc.cjs +54 -0
- package/dist/sessionSummary-CcORiQSq.cjs +393 -0
- package/dist/sessionSummary-DCWQoaDP.mjs +387 -0
- package/dist/spawnHappyCLI-KiazE3kL.cjs +70 -0
- package/dist/spawnHappyCLI-yBIP_Oqi.mjs +66 -0
- package/dist/ssh-q5uZhqBf.mjs +359 -0
- package/dist/ssh-yMllSHu5.cjs +365 -0
- package/dist/systemdScope-BeOtAkhk.mjs +25 -0
- package/dist/systemdScope-DW3-UoKB.cjs +29 -0
- package/dist/task-_nr7J19j.cjs +167 -0
- package/dist/task-wNX1N2i4.mjs +165 -0
- package/dist/time-Bj2HRkKH.cjs +33 -0
- package/dist/time-DgsBLJA_.mjs +29 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-BoJh4Qfv.mjs +57 -0
- package/dist/types-DWeB7aqh.cjs +59 -0
- package/dist/uninstall-Buaz7d-X.cjs +77 -0
- package/dist/uninstall-Dxf6_4mQ.mjs +75 -0
- package/dist/update-CGlIfa8e.cjs +111 -0
- package/dist/update-CnnoX2cV.mjs +109 -0
- package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
- package/dist/workflowCommon-Bl568-rt.cjs +62 -0
- package/dist/workflowReports-BEWuQesA.cjs +113 -0
- package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
- package/package.json +1 -1
- package/dist/index-BbCDDvoB.cjs +0 -11376
- package/dist/index-vvZXdswy.mjs +0 -11336
- package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
- package/dist/mcpToolSchemas-f-kttjFw.cjs +0 -186
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import tweetnacl from 'tweetnacl';
|
|
3
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
4
|
+
import { r as readCredentials, c as configuration, f as decodeBase64, u as updateSettings, w as writeCredentialsDataKey, o as libsodiumPublicKeyFromSecretKey } from './persistence-DKfF81pd.mjs';
|
|
5
|
+
import { decryptWithEphemeralKey } from './auth-DxJ3VvrX.mjs';
|
|
6
|
+
import 'node:fs/promises';
|
|
7
|
+
import 'node:fs';
|
|
8
|
+
import 'node:os';
|
|
9
|
+
import 'node:path';
|
|
10
|
+
import 'zod';
|
|
11
|
+
import 'chalk';
|
|
12
|
+
import 'fs';
|
|
13
|
+
import 'qrcode-terminal';
|
|
14
|
+
import './time-DgsBLJA_.mjs';
|
|
15
|
+
import './browser-AoVGRX7y.mjs';
|
|
16
|
+
import 'open';
|
|
17
|
+
import 'react';
|
|
18
|
+
import 'ink';
|
|
19
|
+
|
|
20
|
+
async function hmac_sha512(key, data) {
|
|
21
|
+
const hmac = createHmac("sha512", key);
|
|
22
|
+
hmac.update(data);
|
|
23
|
+
return new Uint8Array(hmac.digest());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function deriveSecretKeyTreeRoot(seed, usage) {
|
|
27
|
+
const I = await hmac_sha512(new TextEncoder().encode(usage + " Master Seed"), seed);
|
|
28
|
+
return {
|
|
29
|
+
key: I.slice(0, 32),
|
|
30
|
+
chainCode: I.slice(32)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function deriveSecretKeyTreeChild(chainCode, index) {
|
|
34
|
+
const data = new Uint8Array([0, ...new TextEncoder().encode(index)]);
|
|
35
|
+
const I = await hmac_sha512(chainCode, data);
|
|
36
|
+
return {
|
|
37
|
+
key: I.subarray(0, 32),
|
|
38
|
+
chainCode: I.subarray(32)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function deriveKey(master, usage, path) {
|
|
42
|
+
let state = await deriveSecretKeyTreeRoot(master, usage);
|
|
43
|
+
let remaining = [...path];
|
|
44
|
+
while (remaining.length > 0) {
|
|
45
|
+
let index = remaining[0];
|
|
46
|
+
remaining = remaining.slice(1);
|
|
47
|
+
state = await deriveSecretKeyTreeChild(state.chainCode, index);
|
|
48
|
+
}
|
|
49
|
+
return state.key;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseEnrollToken(raw) {
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
const separator = trimmed.indexOf(".");
|
|
55
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
56
|
+
throw new Error("Invalid enrollment token format");
|
|
57
|
+
}
|
|
58
|
+
const lookupId = trimmed.slice(0, separator);
|
|
59
|
+
const secret = decodeBase64(trimmed.slice(separator + 1), "base64url");
|
|
60
|
+
if (secret.length !== 32) {
|
|
61
|
+
throw new Error("Invalid enrollment token secret");
|
|
62
|
+
}
|
|
63
|
+
return { lookupId, secret };
|
|
64
|
+
}
|
|
65
|
+
async function enrollDevice(rawToken, options = {}) {
|
|
66
|
+
const existing = await readCredentials();
|
|
67
|
+
if (existing && !options.force) {
|
|
68
|
+
return { alreadyEnrolled: true };
|
|
69
|
+
}
|
|
70
|
+
const { lookupId, secret } = parseEnrollToken(rawToken);
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
const result = await axios.post(
|
|
74
|
+
`${configuration.serverUrl}/v1/devices/enroll`,
|
|
75
|
+
{ lookupId },
|
|
76
|
+
{ timeout: 3e4 }
|
|
77
|
+
);
|
|
78
|
+
response = result.data;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
81
|
+
const message = error.response.data?.error ?? `HTTP ${error.response.status}`;
|
|
82
|
+
throw new Error(`Enrollment failed: ${message}`);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const sealed = decodeBase64(response.response);
|
|
87
|
+
const boxSecretKey = tweetnacl.hash(secret).slice(0, 32);
|
|
88
|
+
const opened = decryptWithEphemeralKey(sealed, boxSecretKey);
|
|
89
|
+
if (!opened) {
|
|
90
|
+
throw new Error("Enrollment failed: could not open the account key with this token");
|
|
91
|
+
}
|
|
92
|
+
await updateSettings((settings) => ({ ...settings, deviceMode: true }));
|
|
93
|
+
if (opened.length === 32) {
|
|
94
|
+
const contentDataKey = await deriveKey(opened, "Happy EnCoder", ["content"]);
|
|
95
|
+
await writeCredentialsDataKey({
|
|
96
|
+
publicKey: libsodiumPublicKeyFromSecretKey(contentDataKey),
|
|
97
|
+
machineKey: randomBytes(32),
|
|
98
|
+
token: response.token
|
|
99
|
+
});
|
|
100
|
+
return { alreadyEnrolled: false };
|
|
101
|
+
}
|
|
102
|
+
if (opened[0] === 0 && opened.length >= 33) {
|
|
103
|
+
await writeCredentialsDataKey({
|
|
104
|
+
publicKey: opened.slice(1, 33),
|
|
105
|
+
machineKey: randomBytes(32),
|
|
106
|
+
token: response.token
|
|
107
|
+
});
|
|
108
|
+
return { alreadyEnrolled: false };
|
|
109
|
+
}
|
|
110
|
+
throw new Error("Enrollment failed: unsupported account key format");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { enrollDevice };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var tweetnacl = require('tweetnacl');
|
|
5
|
+
var node_crypto = require('node:crypto');
|
|
6
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
7
|
+
var auth = require('./auth-C7ZScWEw.cjs');
|
|
8
|
+
require('node:fs/promises');
|
|
9
|
+
require('node:fs');
|
|
10
|
+
require('node:os');
|
|
11
|
+
require('node:path');
|
|
12
|
+
require('zod');
|
|
13
|
+
require('chalk');
|
|
14
|
+
require('fs');
|
|
15
|
+
require('qrcode-terminal');
|
|
16
|
+
require('./time-Bj2HRkKH.cjs');
|
|
17
|
+
require('./browser-DzgO4oaD.cjs');
|
|
18
|
+
require('open');
|
|
19
|
+
require('react');
|
|
20
|
+
require('ink');
|
|
21
|
+
|
|
22
|
+
async function hmac_sha512(key, data) {
|
|
23
|
+
const hmac = node_crypto.createHmac("sha512", key);
|
|
24
|
+
hmac.update(data);
|
|
25
|
+
return new Uint8Array(hmac.digest());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function deriveSecretKeyTreeRoot(seed, usage) {
|
|
29
|
+
const I = await hmac_sha512(new TextEncoder().encode(usage + " Master Seed"), seed);
|
|
30
|
+
return {
|
|
31
|
+
key: I.slice(0, 32),
|
|
32
|
+
chainCode: I.slice(32)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function deriveSecretKeyTreeChild(chainCode, index) {
|
|
36
|
+
const data = new Uint8Array([0, ...new TextEncoder().encode(index)]);
|
|
37
|
+
const I = await hmac_sha512(chainCode, data);
|
|
38
|
+
return {
|
|
39
|
+
key: I.subarray(0, 32),
|
|
40
|
+
chainCode: I.subarray(32)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function deriveKey(master, usage, path) {
|
|
44
|
+
let state = await deriveSecretKeyTreeRoot(master, usage);
|
|
45
|
+
let remaining = [...path];
|
|
46
|
+
while (remaining.length > 0) {
|
|
47
|
+
let index = remaining[0];
|
|
48
|
+
remaining = remaining.slice(1);
|
|
49
|
+
state = await deriveSecretKeyTreeChild(state.chainCode, index);
|
|
50
|
+
}
|
|
51
|
+
return state.key;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseEnrollToken(raw) {
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
const separator = trimmed.indexOf(".");
|
|
57
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
58
|
+
throw new Error("Invalid enrollment token format");
|
|
59
|
+
}
|
|
60
|
+
const lookupId = trimmed.slice(0, separator);
|
|
61
|
+
const secret = logger.decodeBase64(trimmed.slice(separator + 1), "base64url");
|
|
62
|
+
if (secret.length !== 32) {
|
|
63
|
+
throw new Error("Invalid enrollment token secret");
|
|
64
|
+
}
|
|
65
|
+
return { lookupId, secret };
|
|
66
|
+
}
|
|
67
|
+
async function enrollDevice(rawToken, options = {}) {
|
|
68
|
+
const existing = await logger.readCredentials();
|
|
69
|
+
if (existing && !options.force) {
|
|
70
|
+
return { alreadyEnrolled: true };
|
|
71
|
+
}
|
|
72
|
+
const { lookupId, secret } = parseEnrollToken(rawToken);
|
|
73
|
+
let response;
|
|
74
|
+
try {
|
|
75
|
+
const result = await axios.post(
|
|
76
|
+
`${logger.configuration.serverUrl}/v1/devices/enroll`,
|
|
77
|
+
{ lookupId },
|
|
78
|
+
{ timeout: 3e4 }
|
|
79
|
+
);
|
|
80
|
+
response = result.data;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
83
|
+
const message = error.response.data?.error ?? `HTTP ${error.response.status}`;
|
|
84
|
+
throw new Error(`Enrollment failed: ${message}`);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
const sealed = logger.decodeBase64(response.response);
|
|
89
|
+
const boxSecretKey = tweetnacl.hash(secret).slice(0, 32);
|
|
90
|
+
const opened = auth.decryptWithEphemeralKey(sealed, boxSecretKey);
|
|
91
|
+
if (!opened) {
|
|
92
|
+
throw new Error("Enrollment failed: could not open the account key with this token");
|
|
93
|
+
}
|
|
94
|
+
await logger.updateSettings((settings) => ({ ...settings, deviceMode: true }));
|
|
95
|
+
if (opened.length === 32) {
|
|
96
|
+
const contentDataKey = await deriveKey(opened, "Happy EnCoder", ["content"]);
|
|
97
|
+
await logger.writeCredentialsDataKey({
|
|
98
|
+
publicKey: logger.libsodiumPublicKeyFromSecretKey(contentDataKey),
|
|
99
|
+
machineKey: node_crypto.randomBytes(32),
|
|
100
|
+
token: response.token
|
|
101
|
+
});
|
|
102
|
+
return { alreadyEnrolled: false };
|
|
103
|
+
}
|
|
104
|
+
if (opened[0] === 0 && opened.length >= 33) {
|
|
105
|
+
await logger.writeCredentialsDataKey({
|
|
106
|
+
publicKey: opened.slice(1, 33),
|
|
107
|
+
machineKey: node_crypto.randomBytes(32),
|
|
108
|
+
token: response.token
|
|
109
|
+
});
|
|
110
|
+
return { alreadyEnrolled: false };
|
|
111
|
+
}
|
|
112
|
+
throw new Error("Enrollment failed: unsupported account key format");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
exports.enrollDevice = enrollDevice;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chalk = require('chalk');
|
|
4
|
+
var fs = require('node:fs');
|
|
5
|
+
var promises = require('node:fs/promises');
|
|
6
|
+
var node_crypto = require('node:crypto');
|
|
7
|
+
var path = require('node:path');
|
|
8
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
9
|
+
var workflowReports = require('./workflowReports-BEWuQesA.cjs');
|
|
10
|
+
var workflowCommon = require('./workflowCommon-Bl568-rt.cjs');
|
|
11
|
+
require('node:os');
|
|
12
|
+
require('zod');
|
|
13
|
+
require('tweetnacl');
|
|
14
|
+
require('fs');
|
|
15
|
+
|
|
16
|
+
const EVIDENCE_FILE = `${logger.configuration.happyHomeDir}/evidence/evidence.jsonl`;
|
|
17
|
+
function printEvidenceHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
${chalk.bold("happy evidence")} - Collect and render UI/debug evidence
|
|
20
|
+
|
|
21
|
+
${chalk.bold("Usage:")}
|
|
22
|
+
happy evidence add <path-or-url> [options]
|
|
23
|
+
happy evidence list [options]
|
|
24
|
+
happy evidence report [options]
|
|
25
|
+
|
|
26
|
+
${chalk.bold("Options:")}
|
|
27
|
+
--title <text> Human-readable evidence title
|
|
28
|
+
--kind <kind> image | log | url | file (default: inferred)
|
|
29
|
+
--session <id> Attach evidence to a Happy session
|
|
30
|
+
--note <text> Short note shown in reports
|
|
31
|
+
--out <path> Write report/list markdown to a file
|
|
32
|
+
--json Print machine-readable JSON
|
|
33
|
+
-h, --help Show this help
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
function normalizeEvidenceTarget(target) {
|
|
37
|
+
if (/^(https?:|file:)/i.test(target)) return target;
|
|
38
|
+
return path.resolve(target);
|
|
39
|
+
}
|
|
40
|
+
async function readEvidenceEntries() {
|
|
41
|
+
try {
|
|
42
|
+
const content = await promises.readFile(EVIDENCE_FILE, "utf8");
|
|
43
|
+
return content.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code === "ENOENT") return [];
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function filterEvidenceEntries(entries, sessionId) {
|
|
50
|
+
if (!sessionId) return entries;
|
|
51
|
+
return entries.filter((entry) => entry.sessionId === sessionId);
|
|
52
|
+
}
|
|
53
|
+
async function assertLocalFileExists(target) {
|
|
54
|
+
if (/^https?:/i.test(target) || /^file:/i.test(target)) return;
|
|
55
|
+
await promises.access(target, fs.constants.R_OK);
|
|
56
|
+
}
|
|
57
|
+
function parseKind(kind, target) {
|
|
58
|
+
if (!kind) return workflowReports.inferEvidenceKind(target);
|
|
59
|
+
if (kind === "image" || kind === "log" || kind === "url" || kind === "file") return kind;
|
|
60
|
+
throw new Error("--kind must be one of: image, log, url, file.");
|
|
61
|
+
}
|
|
62
|
+
async function addEvidence(args) {
|
|
63
|
+
const parsed = workflowCommon.parseWorkflowArgs(args, /* @__PURE__ */ new Set(["--title", "--kind", "--session", "--note"]));
|
|
64
|
+
const target = parsed.positionals[0];
|
|
65
|
+
if (!target) {
|
|
66
|
+
throw new Error('Evidence target required. Use "happy evidence add <path-or-url>".');
|
|
67
|
+
}
|
|
68
|
+
const normalizedTarget = normalizeEvidenceTarget(target);
|
|
69
|
+
await assertLocalFileExists(normalizedTarget);
|
|
70
|
+
const entry = {
|
|
71
|
+
id: node_crypto.randomUUID(),
|
|
72
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
73
|
+
target: normalizedTarget,
|
|
74
|
+
title: workflowCommon.stringFlag(parsed, "--title") ?? path.basename(normalizedTarget),
|
|
75
|
+
kind: parseKind(workflowCommon.stringFlag(parsed, "--kind"), normalizedTarget),
|
|
76
|
+
sessionId: workflowCommon.stringFlag(parsed, "--session"),
|
|
77
|
+
note: workflowCommon.stringFlag(parsed, "--note")
|
|
78
|
+
};
|
|
79
|
+
await promises.mkdir(path.dirname(EVIDENCE_FILE), { recursive: true });
|
|
80
|
+
await promises.appendFile(EVIDENCE_FILE, `${JSON.stringify(entry)}
|
|
81
|
+
`, "utf8");
|
|
82
|
+
if (workflowCommon.booleanFlag(parsed, "--json")) {
|
|
83
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`Added evidence ${entry.id}: ${entry.target}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function listEvidence(args) {
|
|
89
|
+
const parsed = workflowCommon.parseWorkflowArgs(args, /* @__PURE__ */ new Set(["--session", "--out"]));
|
|
90
|
+
const entries = filterEvidenceEntries(await readEvidenceEntries(), workflowCommon.stringFlag(parsed, "--session"));
|
|
91
|
+
if (workflowCommon.booleanFlag(parsed, "--json")) {
|
|
92
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const output = entries.length === 0 ? "No evidence recorded." : entries.map((entry) => `- ${entry.createdAt} [${entry.kind}] ${entry.title}: ${entry.target}`).join("\n");
|
|
96
|
+
await workflowCommon.printOrWriteOutput(output, workflowCommon.stringFlag(parsed, "--out"));
|
|
97
|
+
}
|
|
98
|
+
async function reportEvidence(args) {
|
|
99
|
+
const parsed = workflowCommon.parseWorkflowArgs(args, /* @__PURE__ */ new Set(["--session", "--out", "--title"]));
|
|
100
|
+
const entries = filterEvidenceEntries(await readEvidenceEntries(), workflowCommon.stringFlag(parsed, "--session"));
|
|
101
|
+
const title = workflowCommon.stringFlag(parsed, "--title") ?? "Evidence Report";
|
|
102
|
+
if (workflowCommon.booleanFlag(parsed, "--json")) {
|
|
103
|
+
console.log(JSON.stringify({ title, entries }, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await workflowCommon.printOrWriteOutput(workflowReports.renderEvidenceReport(entries, title), workflowCommon.stringFlag(parsed, "--out"));
|
|
107
|
+
}
|
|
108
|
+
async function handleEvidenceCommand(args) {
|
|
109
|
+
const action = args[0];
|
|
110
|
+
if (!action || action === "-h" || action === "--help") {
|
|
111
|
+
printEvidenceHelp();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (action === "add") {
|
|
115
|
+
await addEvidence(args.slice(1));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (action === "list") {
|
|
119
|
+
await listEvidence(args.slice(1));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (action === "report") {
|
|
123
|
+
await reportEvidence(args.slice(1));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Unknown evidence command: ${action}. Use add, list, or report.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
exports.handleEvidenceCommand = handleEvidenceCommand;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { mkdir, appendFile, access, readFile } from 'node:fs/promises';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
6
|
+
import { c as configuration } from './persistence-DKfF81pd.mjs';
|
|
7
|
+
import { r as renderEvidenceReport, i as inferEvidenceKind } from './workflowReports-Dd8-4-8f.mjs';
|
|
8
|
+
import { p as parseWorkflowArgs, s as stringFlag, b as booleanFlag, a as printOrWriteOutput } from './workflowCommon-B0d01Y_a.mjs';
|
|
9
|
+
import 'node:os';
|
|
10
|
+
import 'zod';
|
|
11
|
+
import 'tweetnacl';
|
|
12
|
+
import 'fs';
|
|
13
|
+
|
|
14
|
+
const EVIDENCE_FILE = `${configuration.happyHomeDir}/evidence/evidence.jsonl`;
|
|
15
|
+
function printEvidenceHelp() {
|
|
16
|
+
console.log(`
|
|
17
|
+
${chalk.bold("happy evidence")} - Collect and render UI/debug evidence
|
|
18
|
+
|
|
19
|
+
${chalk.bold("Usage:")}
|
|
20
|
+
happy evidence add <path-or-url> [options]
|
|
21
|
+
happy evidence list [options]
|
|
22
|
+
happy evidence report [options]
|
|
23
|
+
|
|
24
|
+
${chalk.bold("Options:")}
|
|
25
|
+
--title <text> Human-readable evidence title
|
|
26
|
+
--kind <kind> image | log | url | file (default: inferred)
|
|
27
|
+
--session <id> Attach evidence to a Happy session
|
|
28
|
+
--note <text> Short note shown in reports
|
|
29
|
+
--out <path> Write report/list markdown to a file
|
|
30
|
+
--json Print machine-readable JSON
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
function normalizeEvidenceTarget(target) {
|
|
35
|
+
if (/^(https?:|file:)/i.test(target)) return target;
|
|
36
|
+
return resolve(target);
|
|
37
|
+
}
|
|
38
|
+
async function readEvidenceEntries() {
|
|
39
|
+
try {
|
|
40
|
+
const content = await readFile(EVIDENCE_FILE, "utf8");
|
|
41
|
+
return content.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code === "ENOENT") return [];
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function filterEvidenceEntries(entries, sessionId) {
|
|
48
|
+
if (!sessionId) return entries;
|
|
49
|
+
return entries.filter((entry) => entry.sessionId === sessionId);
|
|
50
|
+
}
|
|
51
|
+
async function assertLocalFileExists(target) {
|
|
52
|
+
if (/^https?:/i.test(target) || /^file:/i.test(target)) return;
|
|
53
|
+
await access(target, constants.R_OK);
|
|
54
|
+
}
|
|
55
|
+
function parseKind(kind, target) {
|
|
56
|
+
if (!kind) return inferEvidenceKind(target);
|
|
57
|
+
if (kind === "image" || kind === "log" || kind === "url" || kind === "file") return kind;
|
|
58
|
+
throw new Error("--kind must be one of: image, log, url, file.");
|
|
59
|
+
}
|
|
60
|
+
async function addEvidence(args) {
|
|
61
|
+
const parsed = parseWorkflowArgs(args, /* @__PURE__ */ new Set(["--title", "--kind", "--session", "--note"]));
|
|
62
|
+
const target = parsed.positionals[0];
|
|
63
|
+
if (!target) {
|
|
64
|
+
throw new Error('Evidence target required. Use "happy evidence add <path-or-url>".');
|
|
65
|
+
}
|
|
66
|
+
const normalizedTarget = normalizeEvidenceTarget(target);
|
|
67
|
+
await assertLocalFileExists(normalizedTarget);
|
|
68
|
+
const entry = {
|
|
69
|
+
id: randomUUID(),
|
|
70
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71
|
+
target: normalizedTarget,
|
|
72
|
+
title: stringFlag(parsed, "--title") ?? basename(normalizedTarget),
|
|
73
|
+
kind: parseKind(stringFlag(parsed, "--kind"), normalizedTarget),
|
|
74
|
+
sessionId: stringFlag(parsed, "--session"),
|
|
75
|
+
note: stringFlag(parsed, "--note")
|
|
76
|
+
};
|
|
77
|
+
await mkdir(dirname(EVIDENCE_FILE), { recursive: true });
|
|
78
|
+
await appendFile(EVIDENCE_FILE, `${JSON.stringify(entry)}
|
|
79
|
+
`, "utf8");
|
|
80
|
+
if (booleanFlag(parsed, "--json")) {
|
|
81
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
82
|
+
} else {
|
|
83
|
+
console.log(`Added evidence ${entry.id}: ${entry.target}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function listEvidence(args) {
|
|
87
|
+
const parsed = parseWorkflowArgs(args, /* @__PURE__ */ new Set(["--session", "--out"]));
|
|
88
|
+
const entries = filterEvidenceEntries(await readEvidenceEntries(), stringFlag(parsed, "--session"));
|
|
89
|
+
if (booleanFlag(parsed, "--json")) {
|
|
90
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const output = entries.length === 0 ? "No evidence recorded." : entries.map((entry) => `- ${entry.createdAt} [${entry.kind}] ${entry.title}: ${entry.target}`).join("\n");
|
|
94
|
+
await printOrWriteOutput(output, stringFlag(parsed, "--out"));
|
|
95
|
+
}
|
|
96
|
+
async function reportEvidence(args) {
|
|
97
|
+
const parsed = parseWorkflowArgs(args, /* @__PURE__ */ new Set(["--session", "--out", "--title"]));
|
|
98
|
+
const entries = filterEvidenceEntries(await readEvidenceEntries(), stringFlag(parsed, "--session"));
|
|
99
|
+
const title = stringFlag(parsed, "--title") ?? "Evidence Report";
|
|
100
|
+
if (booleanFlag(parsed, "--json")) {
|
|
101
|
+
console.log(JSON.stringify({ title, entries }, null, 2));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await printOrWriteOutput(renderEvidenceReport(entries, title), stringFlag(parsed, "--out"));
|
|
105
|
+
}
|
|
106
|
+
async function handleEvidenceCommand(args) {
|
|
107
|
+
const action = args[0];
|
|
108
|
+
if (!action || action === "-h" || action === "--help") {
|
|
109
|
+
printEvidenceHelp();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (action === "add") {
|
|
113
|
+
await addEvidence(args.slice(1));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (action === "list") {
|
|
117
|
+
await listEvidence(args.slice(1));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (action === "report") {
|
|
121
|
+
await reportEvidence(args.slice(1));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Unknown evidence command: ${action}. Use add, list, or report.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { handleEvidenceCommand };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var downloadImage = require('./downloadImage-cQ1Z7hkC.cjs');
|
|
4
|
+
|
|
5
|
+
async function formatMessageForClaude(text, images) {
|
|
6
|
+
const content = [];
|
|
7
|
+
for (const image of images) {
|
|
8
|
+
const downloaded = await downloadImage.downloadImage(image.url);
|
|
9
|
+
content.push({
|
|
10
|
+
type: "image",
|
|
11
|
+
source: {
|
|
12
|
+
type: "base64",
|
|
13
|
+
media_type: downloaded.mimeType,
|
|
14
|
+
data: downloaded.base64
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (text.trim()) {
|
|
19
|
+
content.push({
|
|
20
|
+
type: "text",
|
|
21
|
+
text
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return content;
|
|
25
|
+
}
|
|
26
|
+
async function formatMessageForGemini(text, images) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
for (const image of images) {
|
|
29
|
+
const downloaded = await downloadImage.downloadImage(image.url);
|
|
30
|
+
parts.push({
|
|
31
|
+
inlineData: {
|
|
32
|
+
mimeType: downloaded.mimeType,
|
|
33
|
+
data: downloaded.base64
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (text.trim()) {
|
|
38
|
+
parts.push({
|
|
39
|
+
text
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return { parts };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
exports.formatMessageForClaude = formatMessageForClaude;
|
|
46
|
+
exports.formatMessageForGemini = formatMessageForGemini;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { d as downloadImage } from './downloadImage-DZXI3mXL.mjs';
|
|
2
|
+
|
|
3
|
+
async function formatMessageForClaude(text, images) {
|
|
4
|
+
const content = [];
|
|
5
|
+
for (const image of images) {
|
|
6
|
+
const downloaded = await downloadImage(image.url);
|
|
7
|
+
content.push({
|
|
8
|
+
type: "image",
|
|
9
|
+
source: {
|
|
10
|
+
type: "base64",
|
|
11
|
+
media_type: downloaded.mimeType,
|
|
12
|
+
data: downloaded.base64
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (text.trim()) {
|
|
17
|
+
content.push({
|
|
18
|
+
type: "text",
|
|
19
|
+
text
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
24
|
+
async function formatMessageForGemini(text, images) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
for (const image of images) {
|
|
27
|
+
const downloaded = await downloadImage(image.url);
|
|
28
|
+
parts.push({
|
|
29
|
+
inlineData: {
|
|
30
|
+
mimeType: downloaded.mimeType,
|
|
31
|
+
data: downloaded.base64
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (text.trim()) {
|
|
36
|
+
parts.push({
|
|
37
|
+
text
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return { parts };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { formatMessageForClaude as a, formatMessageForGemini as f };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { b as buildGithubTakeoverReport } from './workflowReports-Dd8-4-8f.mjs';
|
|
3
|
+
import { p as parseWorkflowArgs, b as booleanFlag, s as stringFlag, a as printOrWriteOutput } from './workflowCommon-B0d01Y_a.mjs';
|
|
4
|
+
import 'node:path';
|
|
5
|
+
import 'node:fs/promises';
|
|
6
|
+
|
|
7
|
+
function printGithubHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
${chalk.bold("happy github")} - GitHub workflow helpers
|
|
10
|
+
|
|
11
|
+
${chalk.bold("Usage:")}
|
|
12
|
+
happy github takeover <issue-or-pr-url-or-number> [options]
|
|
13
|
+
|
|
14
|
+
${chalk.bold("Options:")}
|
|
15
|
+
--repo <owner/name> Repository when target is not a full URL
|
|
16
|
+
--mode <auto|pr|issue> Target type hint (default: auto)
|
|
17
|
+
--out <path> Write the prompt/checklist to a markdown file
|
|
18
|
+
--json Print machine-readable JSON
|
|
19
|
+
-h, --help Show this help
|
|
20
|
+
`);
|
|
21
|
+
}
|
|
22
|
+
async function handleGithubCommand(args) {
|
|
23
|
+
const action = args[0];
|
|
24
|
+
if (!action || action === "-h" || action === "--help") {
|
|
25
|
+
printGithubHelp();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (action !== "takeover") {
|
|
29
|
+
throw new Error(`Unknown github command: ${action}. Use "happy github takeover".`);
|
|
30
|
+
}
|
|
31
|
+
const parsed = parseWorkflowArgs(args.slice(1), /* @__PURE__ */ new Set(["--repo", "--mode", "--out"]));
|
|
32
|
+
if (booleanFlag(parsed, "--help") || booleanFlag(parsed, "-h")) {
|
|
33
|
+
printGithubHelp();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const target = parsed.positionals[0];
|
|
37
|
+
if (!target) {
|
|
38
|
+
throw new Error('Target required. Use "happy github takeover <issue-or-pr-url-or-number>".');
|
|
39
|
+
}
|
|
40
|
+
const mode = stringFlag(parsed, "--mode") ?? "auto";
|
|
41
|
+
if (mode !== "auto" && mode !== "pr" && mode !== "issue") {
|
|
42
|
+
throw new Error("--mode must be one of: auto, pr, issue.");
|
|
43
|
+
}
|
|
44
|
+
const input = {
|
|
45
|
+
target,
|
|
46
|
+
repo: stringFlag(parsed, "--repo"),
|
|
47
|
+
mode
|
|
48
|
+
};
|
|
49
|
+
const report = buildGithubTakeoverReport(input);
|
|
50
|
+
if (booleanFlag(parsed, "--json")) {
|
|
51
|
+
console.log(JSON.stringify({ ...input, report }, null, 2));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await printOrWriteOutput(report, stringFlag(parsed, "--out"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { handleGithubCommand };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chalk = require('chalk');
|
|
4
|
+
var workflowReports = require('./workflowReports-BEWuQesA.cjs');
|
|
5
|
+
var workflowCommon = require('./workflowCommon-Bl568-rt.cjs');
|
|
6
|
+
require('node:path');
|
|
7
|
+
require('node:fs/promises');
|
|
8
|
+
|
|
9
|
+
function printGithubHelp() {
|
|
10
|
+
console.log(`
|
|
11
|
+
${chalk.bold("happy github")} - GitHub workflow helpers
|
|
12
|
+
|
|
13
|
+
${chalk.bold("Usage:")}
|
|
14
|
+
happy github takeover <issue-or-pr-url-or-number> [options]
|
|
15
|
+
|
|
16
|
+
${chalk.bold("Options:")}
|
|
17
|
+
--repo <owner/name> Repository when target is not a full URL
|
|
18
|
+
--mode <auto|pr|issue> Target type hint (default: auto)
|
|
19
|
+
--out <path> Write the prompt/checklist to a markdown file
|
|
20
|
+
--json Print machine-readable JSON
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
async function handleGithubCommand(args) {
|
|
25
|
+
const action = args[0];
|
|
26
|
+
if (!action || action === "-h" || action === "--help") {
|
|
27
|
+
printGithubHelp();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (action !== "takeover") {
|
|
31
|
+
throw new Error(`Unknown github command: ${action}. Use "happy github takeover".`);
|
|
32
|
+
}
|
|
33
|
+
const parsed = workflowCommon.parseWorkflowArgs(args.slice(1), /* @__PURE__ */ new Set(["--repo", "--mode", "--out"]));
|
|
34
|
+
if (workflowCommon.booleanFlag(parsed, "--help") || workflowCommon.booleanFlag(parsed, "-h")) {
|
|
35
|
+
printGithubHelp();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const target = parsed.positionals[0];
|
|
39
|
+
if (!target) {
|
|
40
|
+
throw new Error('Target required. Use "happy github takeover <issue-or-pr-url-or-number>".');
|
|
41
|
+
}
|
|
42
|
+
const mode = workflowCommon.stringFlag(parsed, "--mode") ?? "auto";
|
|
43
|
+
if (mode !== "auto" && mode !== "pr" && mode !== "issue") {
|
|
44
|
+
throw new Error("--mode must be one of: auto, pr, issue.");
|
|
45
|
+
}
|
|
46
|
+
const input = {
|
|
47
|
+
target,
|
|
48
|
+
repo: workflowCommon.stringFlag(parsed, "--repo"),
|
|
49
|
+
mode
|
|
50
|
+
};
|
|
51
|
+
const report = workflowReports.buildGithubTakeoverReport(input);
|
|
52
|
+
if (workflowCommon.booleanFlag(parsed, "--json")) {
|
|
53
|
+
console.log(JSON.stringify({ ...input, report }, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await workflowCommon.printOrWriteOutput(report, workflowCommon.stringFlag(parsed, "--out"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
exports.handleGithubCommand = handleGithubCommand;
|