harnery 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +8 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +37 -0
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/grep.d.ts +35 -2
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +427 -139
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/init.d.ts +15 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +125 -14
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts +4 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +82 -0
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +14 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/rules/stop-hook.d.ts +8 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +8 -0
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts +2 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +24 -4
- package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +24 -7
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +77 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +210 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +309 -66
- package/dist/core/hooks/events/schema.d.ts +42 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts +7 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +4 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +11 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +28 -6
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +48 -0
- package/dist/core/workflow/billing.d.ts.map +1 -0
- package/dist/core/workflow/billing.js +108 -0
- package/dist/core/workflow/child-env.d.ts +30 -0
- package/dist/core/workflow/child-env.d.ts.map +1 -0
- package/dist/core/workflow/child-env.js +43 -0
- package/dist/core/workflow/engine.d.ts +21 -0
- package/dist/core/workflow/engine.d.ts.map +1 -0
- package/dist/core/workflow/engine.js +349 -0
- package/dist/core/workflow/harnesses.d.ts +17 -0
- package/dist/core/workflow/harnesses.d.ts.map +1 -0
- package/dist/core/workflow/harnesses.js +21 -0
- package/dist/core/workflow/spawn-claude.d.ts +25 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
- package/dist/core/workflow/spawn-claude.js +103 -0
- package/dist/core/workflow/spawn-codex.d.ts +22 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
- package/dist/core/workflow/spawn-codex.js +88 -0
- package/dist/core/workflow/spawn-cursor.d.ts +29 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
- package/dist/core/workflow/spawn-cursor.js +91 -0
- package/dist/core/workflow/types.d.ts +152 -0
- package/dist/core/workflow/types.d.ts.map +1 -0
- package/dist/core/workflow/types.js +9 -0
- package/dist/core/workflow/validate.d.ts +15 -0
- package/dist/core/workflow/validate.d.ts.map +1 -0
- package/dist/core/workflow/validate.js +70 -0
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +8 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +44 -0
- package/src/commands/eml.ts +22 -4
- package/src/commands/grep.ts +535 -142
- package/src/commands/harness.ts +147 -0
- package/src/commands/init.ts +138 -17
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +123 -0
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +15 -6
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/claim-conflict.ts +26 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/rules/stop-hook.ts +17 -0
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +23 -2
- package/src/core/agents/state/heartbeat-writer.ts +33 -7
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +271 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +361 -65
- package/src/core/hooks/events/schema.ts +72 -0
- package/src/core/hooks/harness/events.ts +11 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/hooks/resolve/coord-root.ts +28 -6
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +152 -0
- package/src/core/workflow/child-env.ts +47 -0
- package/src/core/workflow/engine.ts +403 -0
- package/src/core/workflow/harnesses.ts +33 -0
- package/src/core/workflow/spawn-claude.ts +118 -0
- package/src/core/workflow/spawn-codex.ts +93 -0
- package/src/core/workflow/spawn-cursor.ts +108 -0
- package/src/core/workflow/types.ts +160 -0
- package/src/core/workflow/validate.ts +75 -0
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { EmitContext } from "../commander.ts";
|
|
3
|
+
import {
|
|
4
|
+
applyMirror,
|
|
5
|
+
type DesktopAccount,
|
|
6
|
+
findDesktopDataDirs,
|
|
7
|
+
listAccounts,
|
|
8
|
+
planMirror,
|
|
9
|
+
readCliAccount,
|
|
10
|
+
} from "../lib/claude-desktop.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `claude-desktop`: make Claude desktop-app sessions survive account
|
|
14
|
+
* switches.
|
|
15
|
+
*
|
|
16
|
+
* The desktop app scopes its Claude Code session sidebar per signed-in
|
|
17
|
+
* account (plain JSON entry files under
|
|
18
|
+
* `<dataDir>/claude-code-sessions/<account-uuid>/<env-id>/`), while the
|
|
19
|
+
* transcripts live account-agnostically under `~/.claude/projects/`. A user
|
|
20
|
+
* who hops accounts (e.g. when one hits its usage limit) loses sidebar
|
|
21
|
+
* access to every session the other account created — even though the data
|
|
22
|
+
* is all still on disk. `mirror` copies the entry files across account
|
|
23
|
+
* directories so each account's sidebar lists the union; restarting the
|
|
24
|
+
* desktop app picks the copies up, and opening one resumes with full
|
|
25
|
+
* history.
|
|
26
|
+
*/
|
|
27
|
+
export function registerClaudeDesktopCommand(program: Command, emit: EmitContext): void {
|
|
28
|
+
const cmd = program
|
|
29
|
+
.command("claude-desktop")
|
|
30
|
+
.description(
|
|
31
|
+
"Claude desktop-app session index: list accounts/sessions, mirror sessions across accounts",
|
|
32
|
+
)
|
|
33
|
+
.action(() => {
|
|
34
|
+
const dirs = requireDataDirs(emit, undefined);
|
|
35
|
+
for (const dir of dirs) {
|
|
36
|
+
emit.data({
|
|
37
|
+
data_dir: dir,
|
|
38
|
+
accounts: listAccounts(dir).map((a) => accountSummary(a)),
|
|
39
|
+
cli_account: readCliAccount(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cmd
|
|
45
|
+
.command("accounts")
|
|
46
|
+
.description("List account directories + session counts (labels the CLI's account when known)")
|
|
47
|
+
.option("--data-dir <path>", "Explicit desktop-app data directory")
|
|
48
|
+
.action((opts: { dataDir?: string }) => {
|
|
49
|
+
const dirs = requireDataDirs(emit, opts.dataDir);
|
|
50
|
+
const cli = readCliAccount();
|
|
51
|
+
emit.data(
|
|
52
|
+
dirs.map((dir) => ({
|
|
53
|
+
data_dir: dir,
|
|
54
|
+
accounts: listAccounts(dir).map((a) => ({
|
|
55
|
+
...accountSummary(a),
|
|
56
|
+
is_cli_account: cli !== null && a.accountUuid === cli.accountUuid,
|
|
57
|
+
cli_email: cli !== null && a.accountUuid === cli.accountUuid ? cli.email : null,
|
|
58
|
+
})),
|
|
59
|
+
})),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
cmd
|
|
64
|
+
.command("sessions")
|
|
65
|
+
.description("List session entries across every account (newest activity first)")
|
|
66
|
+
.option("--data-dir <path>", "Explicit desktop-app data directory")
|
|
67
|
+
.option("--account <uuid-prefix>", "Only this account (uuid prefix match)")
|
|
68
|
+
.option("--archived", "Include archived entries")
|
|
69
|
+
.action((opts: { dataDir?: string; account?: string; archived?: boolean }) => {
|
|
70
|
+
const dirs = requireDataDirs(emit, opts.dataDir);
|
|
71
|
+
const rows = [];
|
|
72
|
+
for (const dir of dirs) {
|
|
73
|
+
for (const account of listAccounts(dir)) {
|
|
74
|
+
if (opts.account && !account.accountUuid.startsWith(opts.account)) continue;
|
|
75
|
+
for (const e of account.entries) {
|
|
76
|
+
if (e.isArchived && !opts.archived) continue;
|
|
77
|
+
rows.push({
|
|
78
|
+
account_uuid: account.accountUuid,
|
|
79
|
+
cli_session_id: e.cliSessionId,
|
|
80
|
+
title: e.title,
|
|
81
|
+
cwd: e.cwd,
|
|
82
|
+
model: e.model,
|
|
83
|
+
archived: e.isArchived,
|
|
84
|
+
last_activity_at: e.lastActivityAt,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
rows.sort((a, b) => (b.last_activity_at ?? 0) - (a.last_activity_at ?? 0));
|
|
90
|
+
emit.data(rows);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
cmd
|
|
94
|
+
.command("mirror")
|
|
95
|
+
.description(
|
|
96
|
+
"Copy session entries across accounts so each sidebar lists the union. " +
|
|
97
|
+
"Dry-run by default; --yes applies. Restart the desktop app afterward to pick up copies.",
|
|
98
|
+
)
|
|
99
|
+
.option("--data-dir <path>", "Explicit desktop-app data directory")
|
|
100
|
+
.option(
|
|
101
|
+
"--session <id-or-title>",
|
|
102
|
+
"Only sessions matching this cliSessionId or title substring (repeatable)",
|
|
103
|
+
collect,
|
|
104
|
+
[] as string[],
|
|
105
|
+
)
|
|
106
|
+
.option("--all", "Mirror every (non-archived) session")
|
|
107
|
+
.option(
|
|
108
|
+
"--to <uuid-prefix>",
|
|
109
|
+
"Only copy INTO accounts matching this prefix (repeatable)",
|
|
110
|
+
collect,
|
|
111
|
+
[] as string[],
|
|
112
|
+
)
|
|
113
|
+
.option(
|
|
114
|
+
"--from <uuid-prefix>",
|
|
115
|
+
"Only copy FROM accounts matching this prefix (repeatable)",
|
|
116
|
+
collect,
|
|
117
|
+
[] as string[],
|
|
118
|
+
)
|
|
119
|
+
.option("--include-archived", "Also mirror archived entries")
|
|
120
|
+
.option("--yes", "Apply the plan (default is dry-run)")
|
|
121
|
+
.action(
|
|
122
|
+
(opts: {
|
|
123
|
+
dataDir?: string;
|
|
124
|
+
session: string[];
|
|
125
|
+
all?: boolean;
|
|
126
|
+
to: string[];
|
|
127
|
+
from: string[];
|
|
128
|
+
includeArchived?: boolean;
|
|
129
|
+
yes?: boolean;
|
|
130
|
+
}) => {
|
|
131
|
+
if (!opts.all && opts.session.length === 0) {
|
|
132
|
+
emit.error({
|
|
133
|
+
code: "no_selection",
|
|
134
|
+
message: "pass --session <id-or-title> (repeatable) or --all",
|
|
135
|
+
});
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
const dirs = requireDataDirs(emit, opts.dataDir);
|
|
139
|
+
for (const dir of dirs) {
|
|
140
|
+
const accounts = listAccounts(dir);
|
|
141
|
+
if (accounts.length < 2) {
|
|
142
|
+
emit.error({
|
|
143
|
+
code: "single_account",
|
|
144
|
+
message: `only ${accounts.length} account dir(s) under ${dir} — nothing to mirror across`,
|
|
145
|
+
});
|
|
146
|
+
process.exit(2);
|
|
147
|
+
}
|
|
148
|
+
const plan = planMirror(accounts, {
|
|
149
|
+
to: opts.to,
|
|
150
|
+
from: opts.from,
|
|
151
|
+
sessions: opts.all ? undefined : opts.session,
|
|
152
|
+
includeArchived: opts.includeArchived,
|
|
153
|
+
});
|
|
154
|
+
const planned = plan.actions.map((a) => ({
|
|
155
|
+
title: a.entry.title,
|
|
156
|
+
cli_session_id: a.entry.cliSessionId,
|
|
157
|
+
to_account: a.targetAccountUuid,
|
|
158
|
+
to_file: a.to,
|
|
159
|
+
}));
|
|
160
|
+
if (!opts.yes) {
|
|
161
|
+
emit.data({
|
|
162
|
+
data_dir: dir,
|
|
163
|
+
dry_run: true,
|
|
164
|
+
planned,
|
|
165
|
+
skipped_existing: plan.skippedExisting,
|
|
166
|
+
skipped_archived: plan.skippedArchived,
|
|
167
|
+
hint: plan.actions.length > 0 ? "re-run with --yes to apply" : "nothing to copy",
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const { copied } = applyMirror(plan);
|
|
172
|
+
emit.data({
|
|
173
|
+
data_dir: dir,
|
|
174
|
+
dry_run: false,
|
|
175
|
+
copied,
|
|
176
|
+
planned,
|
|
177
|
+
skipped_existing: plan.skippedExisting,
|
|
178
|
+
skipped_archived: plan.skippedArchived,
|
|
179
|
+
hint:
|
|
180
|
+
copied > 0
|
|
181
|
+
? "fully quit the Claude desktop app (tray icon, not the X) and relaunch to see the sessions"
|
|
182
|
+
: "nothing to copy",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function collect(value: string, previous: string[]): string[] {
|
|
190
|
+
return previous.concat([value]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function accountSummary(a: DesktopAccount) {
|
|
194
|
+
return {
|
|
195
|
+
account_uuid: a.accountUuid,
|
|
196
|
+
sessions: a.entries.length,
|
|
197
|
+
archived: a.entries.filter((e) => e.isArchived).length,
|
|
198
|
+
latest_title: a.entries[0]?.title ?? null,
|
|
199
|
+
latest_activity_at: a.entries[0]?.lastActivityAt ?? null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function requireDataDirs(emit: EmitContext, explicit: string | undefined): string[] {
|
|
204
|
+
const dirs = findDesktopDataDirs(explicit);
|
|
205
|
+
if (dirs.length === 0) {
|
|
206
|
+
emit.error({
|
|
207
|
+
code: "not_found",
|
|
208
|
+
message:
|
|
209
|
+
"no Claude desktop-app data directory with a claude-code-sessions/ index found " +
|
|
210
|
+
"(is the desktop app installed? pass --data-dir or set HARNERY_CLAUDE_DESKTOP_DIR)",
|
|
211
|
+
});
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
return dirs;
|
|
215
|
+
}
|
package/src/commands/context.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
3
|
+
import { monorepoRoot, readHeartbeat, resolveOwner } from "../core/agents/index.ts";
|
|
4
|
+
import {
|
|
5
|
+
type CheckpointReason,
|
|
6
|
+
checkpointContext,
|
|
7
|
+
readContextState,
|
|
8
|
+
readLatestCapsule,
|
|
9
|
+
} from "../core/context/index.ts";
|
|
10
|
+
import type { Harness } from "../core/hooks/events/schema.ts";
|
|
3
11
|
import {
|
|
4
12
|
buildContext,
|
|
5
13
|
type ContextReport,
|
|
@@ -20,7 +28,7 @@ export function registerContextCommand(
|
|
|
20
28
|
emit: EmitContext,
|
|
21
29
|
context?: HarneryProgramContext,
|
|
22
30
|
): void {
|
|
23
|
-
program
|
|
31
|
+
const command = program
|
|
24
32
|
.command("context")
|
|
25
33
|
.description(
|
|
26
34
|
`One-shot orientation snapshot: self, repo, submodules, peers, recent commits. Default sections: ${DEFAULT_SECTIONS.join(" / ")}. Opt-in: ${OPT_IN_SECTIONS.join(" / ")}.`,
|
|
@@ -68,6 +76,171 @@ export function registerContextCommand(
|
|
|
68
76
|
}
|
|
69
77
|
},
|
|
70
78
|
);
|
|
79
|
+
|
|
80
|
+
command
|
|
81
|
+
.command("status")
|
|
82
|
+
.description("Show context-continuity phase, generation, and latest telemetry")
|
|
83
|
+
.option("--session <id>", "Session id (defaults to the current agent heartbeat)")
|
|
84
|
+
.option("--instance <id>", "Agent instance id (defaults to the current owner)")
|
|
85
|
+
.option("--json", "Structured JSON output")
|
|
86
|
+
.action((opts: { session?: string; instance?: string; json?: boolean }) => {
|
|
87
|
+
try {
|
|
88
|
+
const json = opts.json || Boolean(command.opts().json);
|
|
89
|
+
const identity = resolveContinuityIdentity(context, opts);
|
|
90
|
+
const state = readContextState(identity.coordRoot, identity.sessionId);
|
|
91
|
+
if (json) {
|
|
92
|
+
emit.config({ format: "json" });
|
|
93
|
+
emit.data({ state, identity });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!state) {
|
|
97
|
+
emit.text(`No context continuity state for session ${identity.sessionId}.\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const sample = state.latest_context;
|
|
101
|
+
const usage = sample
|
|
102
|
+
? `${sample.used_tokens ?? "?"}/${sample.window_tokens ?? "?"} tokens${sample.used_percent === undefined ? "" : ` (${sample.used_percent}%)`}`
|
|
103
|
+
: "not reported by harness";
|
|
104
|
+
emit.text(
|
|
105
|
+
`${[
|
|
106
|
+
`Session: ${state.session_id}`,
|
|
107
|
+
`Phase: ${state.phase}`,
|
|
108
|
+
`Generation: ${state.generation}`,
|
|
109
|
+
`Telemetry: ${usage}`,
|
|
110
|
+
`Capsule: ${state.latest_capsule ?? "none"}`,
|
|
111
|
+
state.degraded_reason ? `Degraded: ${state.degraded_reason}` : "",
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join("\n")}\n`,
|
|
115
|
+
);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
emitCommandError(emit, "context_status_failed", err);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
command
|
|
122
|
+
.command("checkpoint")
|
|
123
|
+
.description("Create a durable context-continuity capsule for the current work")
|
|
124
|
+
.option("--session <id>", "Session id (defaults to the current agent heartbeat)")
|
|
125
|
+
.option("--instance <id>", "Agent instance id (defaults to the current owner)")
|
|
126
|
+
.option("--harness <id>", "claude-code, codex, or cursor (inferred from heartbeat)")
|
|
127
|
+
.option("--reason <reason>", "manual, pressure, pre_compact, or session_end", "manual")
|
|
128
|
+
.option("--note <text>", "Short continuation note for the recovered agent")
|
|
129
|
+
.option("--json", "Structured JSON output")
|
|
130
|
+
.action(
|
|
131
|
+
(opts: {
|
|
132
|
+
session?: string;
|
|
133
|
+
instance?: string;
|
|
134
|
+
harness?: string;
|
|
135
|
+
reason: string;
|
|
136
|
+
note?: string;
|
|
137
|
+
json?: boolean;
|
|
138
|
+
}) => {
|
|
139
|
+
try {
|
|
140
|
+
const json = opts.json || Boolean(command.opts().json);
|
|
141
|
+
const reason = parseCheckpointReason(opts.reason);
|
|
142
|
+
const identity = resolveContinuityIdentity(context, opts);
|
|
143
|
+
if (!identity.harness) {
|
|
144
|
+
throw new Error("could not infer the harness; pass --harness claude-code|codex|cursor");
|
|
145
|
+
}
|
|
146
|
+
const result = checkpointContext(identity.coordRoot, {
|
|
147
|
+
sessionId: identity.sessionId,
|
|
148
|
+
instanceId: identity.instanceId,
|
|
149
|
+
harness: identity.harness,
|
|
150
|
+
cwd: context?.repoRoot ?? process.cwd(),
|
|
151
|
+
reason,
|
|
152
|
+
continuationNote: opts.note,
|
|
153
|
+
});
|
|
154
|
+
const output = {
|
|
155
|
+
capsule_id: result.capsule.capsule_id,
|
|
156
|
+
generation: result.capsule.generation,
|
|
157
|
+
path: result.state.latest_capsule,
|
|
158
|
+
phase: result.state.phase,
|
|
159
|
+
reused: result.reused,
|
|
160
|
+
};
|
|
161
|
+
if (json) {
|
|
162
|
+
emit.config({ format: "json" });
|
|
163
|
+
emit.data(output);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
emit.text(
|
|
167
|
+
`Checkpointed context generation ${output.generation}: ${output.path ?? result.path}\n`,
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
emitCommandError(emit, "context_checkpoint_failed", err);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
command
|
|
176
|
+
.command("show")
|
|
177
|
+
.description("Show the latest durable context-continuity capsule")
|
|
178
|
+
.option("--session <id>", "Session id (defaults to the current agent heartbeat)")
|
|
179
|
+
.option("--instance <id>", "Agent instance id (defaults to the current owner)")
|
|
180
|
+
.option("--json", "Structured JSON output")
|
|
181
|
+
.action((opts: { session?: string; instance?: string; json?: boolean }) => {
|
|
182
|
+
try {
|
|
183
|
+
const json = opts.json || Boolean(command.opts().json);
|
|
184
|
+
const identity = resolveContinuityIdentity(context, opts);
|
|
185
|
+
const capsule = readLatestCapsule(identity.coordRoot, identity.sessionId);
|
|
186
|
+
if (!capsule) {
|
|
187
|
+
throw new Error(`no context capsule exists for session ${identity.sessionId}`);
|
|
188
|
+
}
|
|
189
|
+
if (json) emit.config({ format: "json" });
|
|
190
|
+
emit.data(capsule);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
emitCommandError(emit, "context_show_failed", err);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveContinuityIdentity(
|
|
198
|
+
context: HarneryProgramContext | undefined,
|
|
199
|
+
opts: { session?: string; instance?: string; harness?: string },
|
|
200
|
+
): { coordRoot: string; instanceId: string; sessionId: string; harness: Harness | null } {
|
|
201
|
+
const coordRoot = context?.resolveCoordRoot?.() ?? context?.repoRoot ?? monorepoRoot();
|
|
202
|
+
if (!coordRoot) throw new Error("could not resolve a project containing .harnery/");
|
|
203
|
+
const instanceId = opts.instance ?? resolveOwner();
|
|
204
|
+
if (!instanceId) {
|
|
205
|
+
throw new Error("could not resolve the current agent; pass --instance and --session");
|
|
206
|
+
}
|
|
207
|
+
const heartbeat = readHeartbeat(instanceId);
|
|
208
|
+
const sessionId = opts.session ?? heartbeat?.session_id ?? instanceId;
|
|
209
|
+
const harness = opts.harness
|
|
210
|
+
? parseHarness(opts.harness)
|
|
211
|
+
: harnessFromPlatform(heartbeat?.platform);
|
|
212
|
+
return { coordRoot, instanceId, sessionId, harness };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function harnessFromPlatform(platform: string | undefined): Harness | null {
|
|
216
|
+
if (platform === "claude_code" || platform === "claude-code") return "claude-code";
|
|
217
|
+
if (platform === "cursor") return "cursor";
|
|
218
|
+
if (platform === "codex") return "codex";
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseHarness(value: string): Harness {
|
|
223
|
+
if (value === "claude-code" || value === "codex" || value === "cursor") return value;
|
|
224
|
+
throw new Error(`invalid harness "${value}"; expected claude-code, codex, or cursor`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseCheckpointReason(value: string): CheckpointReason {
|
|
228
|
+
if (
|
|
229
|
+
value === "manual" ||
|
|
230
|
+
value === "pressure" ||
|
|
231
|
+
value === "pre_compact" ||
|
|
232
|
+
value === "session_end"
|
|
233
|
+
) {
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(
|
|
237
|
+
`invalid reason "${value}"; expected manual, pressure, pre_compact, or session_end`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function emitCommandError(emit: EmitContext, code: string, err: unknown): void {
|
|
242
|
+
emit.error({ code, message: err instanceof Error ? err.message : String(err) });
|
|
243
|
+
emit.setExitCode(1);
|
|
71
244
|
}
|
|
72
245
|
|
|
73
246
|
function collectSections(value: string, prev: SectionName[]): SectionName[] {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -18,7 +18,15 @@ import path from "node:path";
|
|
|
18
18
|
import type { Command } from "commander";
|
|
19
19
|
import type { EmitContext } from "../commander.ts";
|
|
20
20
|
import { resolveBinName, ripgrepAutoInstall } from "../core/config.ts";
|
|
21
|
+
import { BUILTIN_HARNESS_IDS } from "../core/harnesses/index.ts";
|
|
21
22
|
import { loadHarnessWiring } from "../core/hooks/harness/wiring.ts";
|
|
23
|
+
import { probeBilling } from "../core/workflow/billing.ts";
|
|
24
|
+
import {
|
|
25
|
+
HARNESS_BINARIES,
|
|
26
|
+
HARNESS_INSTALL_HINTS,
|
|
27
|
+
HARNESS_LOGIN_HINTS,
|
|
28
|
+
} from "../core/workflow/harnesses.ts";
|
|
29
|
+
import type { HarnessName } from "../core/workflow/types.ts";
|
|
22
30
|
import { findRg, installRg, managedRgPath, rgInstallSupported } from "../lib/tools/ripgrep.ts";
|
|
23
31
|
|
|
24
32
|
type Severity = "ok" | "warn" | "fail";
|
|
@@ -96,6 +104,7 @@ export function runChecks(): Check[] {
|
|
|
96
104
|
checkRipgrep(),
|
|
97
105
|
checkHarneryDir(),
|
|
98
106
|
checkHarnessHooks(),
|
|
107
|
+
...BUILTIN_HARNESS_IDS.map(checkWorkflowHarness),
|
|
99
108
|
checkRestic(),
|
|
100
109
|
checkRclone(),
|
|
101
110
|
checkPlaywright(),
|
|
@@ -144,6 +153,41 @@ function whichVersion(bin: string, args: string[] = ["--version"]): { ok: boolea
|
|
|
144
153
|
return { ok: true, out };
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
/**
|
|
157
|
+
* One workflow spawn target: is the harness CLI installed, and how will its
|
|
158
|
+
* headless children bill (subscription login vs API key — see billing.ts)?
|
|
159
|
+
* Missing is a warn, not a fail: workflows degrade to the harnesses you have.
|
|
160
|
+
*/
|
|
161
|
+
function checkWorkflowHarness(harness: HarnessName): Check {
|
|
162
|
+
const bin = HARNESS_BINARIES[harness];
|
|
163
|
+
const name = `workflow:${harness}`;
|
|
164
|
+
const r = whichVersion(bin);
|
|
165
|
+
if (!r.ok) {
|
|
166
|
+
return {
|
|
167
|
+
name,
|
|
168
|
+
severity: "warn",
|
|
169
|
+
detail: `${bin} missing (workflow --harness ${harness} unavailable)`,
|
|
170
|
+
hint: `${HARNESS_INSTALL_HINTS[harness]} then: ${HARNESS_LOGIN_HINTS[harness]}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const probe = probeBilling(harness);
|
|
174
|
+
if (probe.login === "absent" && !probe.apiKeyPresent) {
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
severity: "warn",
|
|
178
|
+
detail: `${r.out} — installed, but no stored login or API key detected`,
|
|
179
|
+
hint: HARNESS_LOGIN_HINTS[harness],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const billing =
|
|
183
|
+
probe.mode === "subscription"
|
|
184
|
+
? probe.login === "present"
|
|
185
|
+
? "billing: subscription"
|
|
186
|
+
: "billing: subscription (login unverifiable, CLI is the authority)"
|
|
187
|
+
: `billing: ${probe.mode} (${probe.apiKeySource})`;
|
|
188
|
+
return { name, severity: "ok", detail: `${r.out} — ${billing}` };
|
|
189
|
+
}
|
|
190
|
+
|
|
147
191
|
function checkNode(): Check {
|
|
148
192
|
const v = process.versions.node;
|
|
149
193
|
const major = Number.parseInt(v.split(".")[0], 10);
|
package/src/commands/eml.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function registerEmlCommand(program: Command, emit: EmitContext): void {
|
|
|
27
27
|
.argument("<file>", "Path to the .eml file")
|
|
28
28
|
.option("-o, --output <path>", "Write output to a file instead of stdout")
|
|
29
29
|
.option("--format <type>", "Output format: markdown, json", "markdown")
|
|
30
|
-
.option("--headers", "Include full email headers
|
|
30
|
+
.option("--headers", "Include the source message's full email headers")
|
|
31
31
|
.option("--attachments", "List attachment filenames and sizes")
|
|
32
32
|
.action(async (file: string, opts: EmlOpts) => {
|
|
33
33
|
try {
|
|
@@ -40,7 +40,7 @@ export function registerEmlCommand(program: Command, emit: EmitContext): void {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
interface EmlOpts {
|
|
43
|
+
export interface EmlOpts {
|
|
44
44
|
output?: string;
|
|
45
45
|
format: string;
|
|
46
46
|
headers?: boolean;
|
|
@@ -88,7 +88,7 @@ async function handleEml(file: string, opts: EmlOpts, emit: EmitContext): Promis
|
|
|
88
88
|
* Extract individual messages from the parsed email.
|
|
89
89
|
* Strategy: use HTML body (Gmail quote structure) first, fall back to plain text.
|
|
90
90
|
*/
|
|
91
|
-
function extractThread(parsed: ParsedMail): ThreadMessage[] {
|
|
91
|
+
export function extractThread(parsed: ParsedMail): ThreadMessage[] {
|
|
92
92
|
if (parsed.html && typeof parsed.html === "string") {
|
|
93
93
|
const messages = extractFromHtml(parsed.html, parsed);
|
|
94
94
|
if (messages.length > 0) return messages;
|
|
@@ -451,7 +451,11 @@ function formatAddress(addr: ParsedMail["from"]): string {
|
|
|
451
451
|
/**
|
|
452
452
|
* Render the thread as markdown.
|
|
453
453
|
*/
|
|
454
|
-
function renderMarkdown(
|
|
454
|
+
export function renderMarkdown(
|
|
455
|
+
parsed: ParsedMail,
|
|
456
|
+
messages: ThreadMessage[],
|
|
457
|
+
opts: EmlOpts,
|
|
458
|
+
): string {
|
|
455
459
|
const subject = parsed.subject ?? "Untitled Thread";
|
|
456
460
|
const participants = new Map<string, boolean>();
|
|
457
461
|
for (const m of messages) {
|
|
@@ -471,6 +475,20 @@ function renderMarkdown(parsed: ParsedMail, messages: ThreadMessage[], opts: Eml
|
|
|
471
475
|
lines.push(`**Date range:** ${minDate} to ${maxDate}`);
|
|
472
476
|
lines.push(`**Messages:** ${messages.length}`);
|
|
473
477
|
|
|
478
|
+
if (opts.headers && parsed.headerLines?.length) {
|
|
479
|
+
// A Gmail thread export is a SINGLE .eml (older messages are quoted HTML,
|
|
480
|
+
// not separate MIME parts), so there is one real header set — the source
|
|
481
|
+
// message's. Render it verbatim.
|
|
482
|
+
lines.push("");
|
|
483
|
+
lines.push("**Source headers:**");
|
|
484
|
+
lines.push("");
|
|
485
|
+
lines.push("```");
|
|
486
|
+
for (const h of parsed.headerLines) {
|
|
487
|
+
lines.push(h.line);
|
|
488
|
+
}
|
|
489
|
+
lines.push("```");
|
|
490
|
+
}
|
|
491
|
+
|
|
474
492
|
if (opts.attachments && parsed.attachments?.length) {
|
|
475
493
|
lines.push("");
|
|
476
494
|
lines.push("**Attachments:**");
|