harnery 0.9.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 +1 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +6 -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 +2 -3
- 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/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- 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.map +1 -1
- package/dist/commands/workflow.js +8 -15
- 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 +4 -2
- 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/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- 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.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +11 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +3 -3
- 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 +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +191 -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 +280 -63
- package/dist/core/hooks/events/schema.d.ts +38 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +3 -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/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 +1 -1
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +7 -1
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +11 -2
- package/dist/core/workflow/harnesses.d.ts +3 -3
- package/dist/core/workflow/harnesses.d.ts.map +1 -1
- package/dist/core/workflow/harnesses.js +8 -17
- package/dist/core/workflow/spawn-claude.d.ts +4 -1
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +39 -18
- package/dist/core/workflow/spawn-codex.d.ts +4 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +44 -22
- package/dist/core/workflow/spawn-cursor.d.ts +4 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +35 -16
- package/dist/core/workflow/types.d.ts +9 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- 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 +6 -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 +2 -3
- package/src/commands/eml.ts +22 -4
- package/src/commands/harness.ts +147 -0
- 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 +9 -18
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +4 -2
- 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/commit-conflict.ts +4 -33
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +10 -1
- package/src/core/agents/state/heartbeat-writer.ts +3 -2
- 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 +248 -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 +331 -62
- package/src/core/hooks/events/schema.ts +68 -0
- package/src/core/hooks/harness/events.ts +3 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- 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 +8 -2
- package/src/core/workflow/engine.ts +11 -2
- package/src/core/workflow/harnesses.ts +14 -19
- package/src/core/workflow/spawn-claude.ts +39 -20
- package/src/core/workflow/spawn-codex.ts +47 -28
- package/src/core/workflow/spawn-cursor.ts +37 -18
- package/src/core/workflow/types.ts +9 -2
- 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
package/src/core/config.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Harnery config reader: the settings the coord/hook layer consults when it
|
|
3
|
+
* can't see the consumer CLI's own process.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (`agent-hook`, `agent-coord`) and the web UI run as harnery itself, so they
|
|
9
|
-
* have no other way to learn the consumer's bin name; `harn init` stamps it here
|
|
10
|
-
* for them to read back. The `files` deny/override section is parsed separately
|
|
11
|
-
* by `web/lib/files.ts`.
|
|
5
|
+
* Two layers, project-over-user (project wins field-by-field):
|
|
6
|
+
* 1. `~/.config/harnery/config.jsonc` — user-global base (optional)
|
|
7
|
+
* 2. `<project-root>/.harnery/config.jsonc` — project override (authoritative)
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Fields owned here: `binName` (host CLI name for agent-facing strings),
|
|
10
|
+
* `hooksSetupHint`, `tools`, `workflow`, `skills`, `presence`, plus the tunable
|
|
11
|
+
* `coord` (heartbeat freshness), `backup` (restic repo/password/prune policy),
|
|
12
|
+
* and `sync` (rclone remote/prefix) sections. The `files` deny/override section
|
|
13
|
+
* is parsed separately by `web/lib/files.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Env vars and CLI flags override any config value per invocation (each accessor
|
|
16
|
+
* documents its own precedence). Dependency-free (no jsonc npm dep) so it runs on
|
|
17
|
+
* both Bun and Node, and so the ADR-009 vendored copies stay portable.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
18
22
|
import { join } from "node:path";
|
|
19
23
|
import { coordEnv } from "../lib/env.ts";
|
|
20
24
|
import { findCoordRoot } from "./hooks/resolve/coord-root.ts";
|
|
@@ -22,6 +26,9 @@ import { findCoordRoot } from "./hooks/resolve/coord-root.ts";
|
|
|
22
26
|
/** The standalone CLI's bin name: the resolution floor when nothing else is set. */
|
|
23
27
|
export const DEFAULT_BIN_NAME = "harn";
|
|
24
28
|
|
|
29
|
+
/** Heartbeat-freshness default (seconds): the sweep window when nothing overrides it. */
|
|
30
|
+
export const DEFAULT_FRESHNESS_SECS = 600;
|
|
31
|
+
|
|
25
32
|
interface HarneryConfig {
|
|
26
33
|
/** Host CLI bin name, stamped by `harn init` for a consumer (e.g. "acme"). */
|
|
27
34
|
binName?: string;
|
|
@@ -46,6 +53,42 @@ interface HarneryConfig {
|
|
|
46
53
|
* scrubbed from child envs) without anyone having to remember the flag.
|
|
47
54
|
*/
|
|
48
55
|
workflow?: { subscriptionOnly?: boolean };
|
|
56
|
+
/**
|
|
57
|
+
* Cross-machine presence (ADR 0016). `{ enabled: false }` opts a repo out of
|
|
58
|
+
* the git-refs transport (publishing `refs/harnery/presence/<machine>` to
|
|
59
|
+
* origin + fetching peers'). Default is ON when an origin remote exists —
|
|
60
|
+
* the zero-config story — and every operation is fail-silent.
|
|
61
|
+
*
|
|
62
|
+
* `relay` (optional) is the live upgrade: a wss:// URL of a presence relay
|
|
63
|
+
* (the reference public one is wss://relay.harnery.com; self-hosters run
|
|
64
|
+
* `harn relay serve` or deploy relay/worker/ to their own Cloudflare
|
|
65
|
+
* account). When set, hooks keep a per-machine daemon connected to the
|
|
66
|
+
* relay for seconds-latency presence; the git-refs transport stays on as
|
|
67
|
+
* the floor. Unset → git-refs only.
|
|
68
|
+
*/
|
|
69
|
+
presence?: { enabled?: boolean; relay?: string };
|
|
70
|
+
/**
|
|
71
|
+
* Coord-layer tunables. `freshness_seconds` is the heartbeat age above which
|
|
72
|
+
* the sweeper prunes an agent (default 600). Read via `coordFreshnessSeconds()`.
|
|
73
|
+
*/
|
|
74
|
+
coord?: { freshness_seconds?: number };
|
|
75
|
+
/**
|
|
76
|
+
* `harn backup` (restic) defaults: `repo` path/URL, `password_file`, and the
|
|
77
|
+
* `keep_daily`/`keep_weekly`/`keep_monthly` prune policy. Read via `backupConfig()`.
|
|
78
|
+
*/
|
|
79
|
+
backup?: {
|
|
80
|
+
repo?: string;
|
|
81
|
+
password_file?: string;
|
|
82
|
+
keep_daily?: number;
|
|
83
|
+
keep_weekly?: number;
|
|
84
|
+
keep_monthly?: number;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* `harn sync` (rclone) defaults: the `remote` name and `prefix` subpath. Read
|
|
88
|
+
* via `syncJsoncConfig()`. `harn sync init` also persists these to
|
|
89
|
+
* `~/.config/harnery/sync.json`, which is consulted as a lower-precedence fallback.
|
|
90
|
+
*/
|
|
91
|
+
sync?: { remote?: string; prefix?: string };
|
|
49
92
|
[k: string]: unknown;
|
|
50
93
|
}
|
|
51
94
|
|
|
@@ -89,29 +132,92 @@ export function stripJsonComments(input: string): string {
|
|
|
89
132
|
return out;
|
|
90
133
|
}
|
|
91
134
|
|
|
92
|
-
|
|
93
|
-
|
|
135
|
+
/**
|
|
136
|
+
* The user-global config file, the lower-precedence base under the project file.
|
|
137
|
+
* Honors `XDG_CONFIG_HOME` (falling back to `~/.config`) per the XDG base-dir spec.
|
|
138
|
+
*/
|
|
139
|
+
function userConfigPath(): string {
|
|
140
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
141
|
+
const base = xdg?.trim() ? xdg : join(homedir(), ".config");
|
|
142
|
+
return join(base, "harnery", "config.jsonc");
|
|
143
|
+
}
|
|
94
144
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let mtimeMs = -1;
|
|
145
|
+
/** File mtime in ms, or -1 when the file can't be stat'd (missing). */
|
|
146
|
+
function statMtime(p: string): number {
|
|
98
147
|
try {
|
|
99
|
-
|
|
148
|
+
return statSync(p).mtimeMs;
|
|
100
149
|
} catch {
|
|
101
|
-
|
|
150
|
+
return -1;
|
|
102
151
|
}
|
|
103
|
-
|
|
104
|
-
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Parse one JSONC config file to an object; missing/unparseable → `{}`. */
|
|
155
|
+
function parseConfigFile(p: string): HarneryConfig {
|
|
105
156
|
try {
|
|
106
157
|
const parsed = JSON.parse(stripJsonComments(readFileSync(p, "utf8"))) as HarneryConfig | null;
|
|
107
|
-
if (parsed && typeof parsed === "object")
|
|
158
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
108
159
|
} catch {
|
|
109
|
-
/* missing or unparseable → defaults (
|
|
160
|
+
/* missing or unparseable → defaults (files-section resolver fails loud; the rest is non-critical) */
|
|
161
|
+
}
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
166
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Deep-merge `override` onto `base` (override wins), recursing into plain
|
|
171
|
+
* objects only so a project that sets just `web.port` doesn't wipe a
|
|
172
|
+
* user-global `web.bind`. Arrays + scalars replace wholesale.
|
|
173
|
+
*/
|
|
174
|
+
function mergeConfig(base: HarneryConfig, override: HarneryConfig): HarneryConfig {
|
|
175
|
+
const out: Record<string, unknown> = { ...base };
|
|
176
|
+
for (const [k, v] of Object.entries(override)) {
|
|
177
|
+
const prev = out[k];
|
|
178
|
+
out[k] = isPlainObject(prev) && isPlainObject(v) ? mergeConfig(prev, v) : v;
|
|
110
179
|
}
|
|
111
|
-
|
|
180
|
+
return out as HarneryConfig;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// mtime-keyed per-process cache (both layers): a stat is cheap, a parse on every render isn't.
|
|
184
|
+
let cache: { root: string; projMtime: number; userMtime: number; cfg: HarneryConfig } | null = null;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The effective config for `root`: user-global (`~/.config/harnery/config.jsonc`)
|
|
188
|
+
* as the base, the project file (`<root>/.harnery/config.jsonc`) merged on top.
|
|
189
|
+
* Project values win field-by-field.
|
|
190
|
+
*/
|
|
191
|
+
function readConfig(root: string): HarneryConfig {
|
|
192
|
+
const projPath = join(root, ".harnery", "config.jsonc");
|
|
193
|
+
const userPath = userConfigPath();
|
|
194
|
+
const projMtime = statMtime(projPath);
|
|
195
|
+
const userMtime = statMtime(userPath);
|
|
196
|
+
if (
|
|
197
|
+
cache &&
|
|
198
|
+
cache.root === root &&
|
|
199
|
+
cache.projMtime === projMtime &&
|
|
200
|
+
cache.userMtime === userMtime
|
|
201
|
+
) {
|
|
202
|
+
return cache.cfg;
|
|
203
|
+
}
|
|
204
|
+
const user = userMtime === -1 ? {} : parseConfigFile(userPath);
|
|
205
|
+
const project = projMtime === -1 ? {} : parseConfigFile(projPath);
|
|
206
|
+
const cfg = mergeConfig(user, project);
|
|
207
|
+
cache = { root, projMtime, userMtime, cfg };
|
|
112
208
|
return cfg;
|
|
113
209
|
}
|
|
114
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The PROJECT config file only — no user-global merge. `pinnedBinName` uses this
|
|
213
|
+
* so a user-global `binName` can never masquerade as a deliberate project pin
|
|
214
|
+
* (the pin guards committed, public surfaces; see `pinnedBinName`).
|
|
215
|
+
*/
|
|
216
|
+
function readProjectConfig(root: string): HarneryConfig {
|
|
217
|
+
const p = join(root, ".harnery", "config.jsonc");
|
|
218
|
+
return statMtime(p) === -1 ? {} : parseConfigFile(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
115
221
|
/**
|
|
116
222
|
* Resolve the host CLI's bin name for user-facing strings. Precedence:
|
|
117
223
|
* 1. `HARNERY_BIN` env (explicit per-process override)
|
|
@@ -141,7 +247,7 @@ export function resolveBinName(coordRoot?: string | null): string {
|
|
|
141
247
|
* CLI would otherwise stamp the host's name into public, committed surfaces).
|
|
142
248
|
*/
|
|
143
249
|
export function pinnedBinName(projectRoot: string): string | null {
|
|
144
|
-
const binName =
|
|
250
|
+
const binName = readProjectConfig(projectRoot).binName;
|
|
145
251
|
return typeof binName === "string" && binName.trim() ? binName.trim() : null;
|
|
146
252
|
}
|
|
147
253
|
|
|
@@ -191,3 +297,121 @@ export function workflowSubscriptionOnly(coordRoot?: string | null): boolean {
|
|
|
191
297
|
if (!root) return false;
|
|
192
298
|
return readConfig(root).workflow?.subscriptionOnly === true;
|
|
193
299
|
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Whether cross-machine presence (ADR 0016) is enabled for this repo.
|
|
303
|
+
* Default ON — the transport itself additionally gates on an origin remote
|
|
304
|
+
* existing and fails silent everywhere. Opt out via
|
|
305
|
+
* `.harnery/config.jsonc` `{ "presence": { "enabled": false } }`;
|
|
306
|
+
* `HARNERY_PRESENCE=1|0` overrides per process.
|
|
307
|
+
*/
|
|
308
|
+
export function presenceEnabled(coordRoot?: string | null): boolean {
|
|
309
|
+
const env = coordEnv("PRESENCE");
|
|
310
|
+
if (env === "1") return true;
|
|
311
|
+
if (env === "0") return false;
|
|
312
|
+
const root = coordRoot ?? findCoordRoot();
|
|
313
|
+
if (!root) return false;
|
|
314
|
+
return readConfig(root).presence?.enabled !== false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The presence relay URL for this repo, or null when the relay transport is
|
|
319
|
+
* not configured (git-refs only). `HARNERY_PRESENCE_RELAY` overrides per
|
|
320
|
+
* process (empty string or "0" disables). Requires `presenceEnabled()` to be
|
|
321
|
+
* true — a disabled presence section disables the relay too.
|
|
322
|
+
*/
|
|
323
|
+
export function presenceRelayUrl(coordRoot?: string | null): string | null {
|
|
324
|
+
const root = coordRoot ?? findCoordRoot();
|
|
325
|
+
if (!presenceEnabled(root)) return null;
|
|
326
|
+
const env = coordEnv("PRESENCE_RELAY");
|
|
327
|
+
if (env !== undefined && env !== null) {
|
|
328
|
+
const t = env.trim();
|
|
329
|
+
return t && t !== "0" ? t : null;
|
|
330
|
+
}
|
|
331
|
+
if (!root) return null;
|
|
332
|
+
const relay = readConfig(root).presence?.relay;
|
|
333
|
+
return typeof relay === "string" && relay.trim() ? relay.trim() : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** A positive integer from `v`, else `fallback`. Floors non-integer numbers. */
|
|
337
|
+
function posIntOr(v: unknown, fallback: number): number {
|
|
338
|
+
const n = typeof v === "number" ? v : Number.NaN;
|
|
339
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The heartbeat-freshness window (seconds): the age above which the sweeper
|
|
344
|
+
* prunes an agent, and the cutoff the `agents` surface uses to fold stale peers.
|
|
345
|
+
* Precedence:
|
|
346
|
+
* 1. `HARNERY_AGENT_COORD_FRESHNESS` env (canonical), or `HARNERY_AGENT_FRESHNESS` (legacy alias)
|
|
347
|
+
* 2. `.harnery/config.jsonc` `coord.freshness_seconds`
|
|
348
|
+
* 3. `600` (10 minutes)
|
|
349
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
350
|
+
*/
|
|
351
|
+
export function coordFreshnessSeconds(coordRoot?: string | null): number {
|
|
352
|
+
const env = coordEnv("AGENT_COORD_FRESHNESS") ?? coordEnv("AGENT_FRESHNESS");
|
|
353
|
+
if (env !== undefined) {
|
|
354
|
+
const n = Number.parseInt(env, 10);
|
|
355
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
356
|
+
}
|
|
357
|
+
const root = coordRoot ?? findCoordRoot();
|
|
358
|
+
if (root) return posIntOr(readConfig(root).coord?.freshness_seconds, DEFAULT_FRESHNESS_SECS);
|
|
359
|
+
return DEFAULT_FRESHNESS_SECS;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Resolved `harn backup` defaults (restic repo/password + prune policy). */
|
|
363
|
+
export interface BackupConfig {
|
|
364
|
+
repo: string;
|
|
365
|
+
passwordFile: string;
|
|
366
|
+
keepDaily: number;
|
|
367
|
+
keepWeekly: number;
|
|
368
|
+
keepMonthly: number;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* `harn backup` (restic) defaults. Per field, precedence is env → config → built-in:
|
|
373
|
+
* repo: `HARNERY_RESTIC_REPO` → `backup.repo` → `~/.cache/harnery/restic-repo`
|
|
374
|
+
* passwordFile: `HARNERY_RESTIC_PASSWORD_FILE` → `backup.password_file` → `~/.config/harnery/restic-password`
|
|
375
|
+
* keepDaily/Weekly/Monthly: `backup.keep_*` → 7 / 4 / 6
|
|
376
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
377
|
+
*/
|
|
378
|
+
export function backupConfig(coordRoot?: string | null): BackupConfig {
|
|
379
|
+
const home = homedir();
|
|
380
|
+
const root = coordRoot ?? findCoordRoot();
|
|
381
|
+
const b = root ? (readConfig(root).backup ?? {}) : {};
|
|
382
|
+
const cfgStr = (v: unknown): string | undefined =>
|
|
383
|
+
typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
384
|
+
const repo =
|
|
385
|
+
coordEnv("RESTIC_REPO") ?? cfgStr(b.repo) ?? join(home, ".cache", "harnery", "restic-repo");
|
|
386
|
+
const passwordFile =
|
|
387
|
+
coordEnv("RESTIC_PASSWORD_FILE") ??
|
|
388
|
+
cfgStr(b.password_file) ??
|
|
389
|
+
join(home, ".config", "harnery", "restic-password");
|
|
390
|
+
return {
|
|
391
|
+
repo,
|
|
392
|
+
passwordFile,
|
|
393
|
+
keepDaily: posIntOr(b.keep_daily, 7),
|
|
394
|
+
keepWeekly: posIntOr(b.keep_weekly, 4),
|
|
395
|
+
keepMonthly: posIntOr(b.keep_monthly, 6),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* `harn sync` (rclone) remote/prefix from `.harnery/config.jsonc` `sync`, or null
|
|
401
|
+
* when unset. This is the config-file layer only; `harn sync` consults env
|
|
402
|
+
* (`HARNERY_SYNC_REMOTE`/`_PREFIX`) first and the `~/.config/harnery/sync.json`
|
|
403
|
+
* file (written by `harn sync init`) as a lower-precedence fallback.
|
|
404
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
405
|
+
*/
|
|
406
|
+
export function syncJsoncConfig(
|
|
407
|
+
coordRoot?: string | null,
|
|
408
|
+
): { remote: string; prefix: string } | null {
|
|
409
|
+
const root = coordRoot ?? findCoordRoot();
|
|
410
|
+
if (!root) return null;
|
|
411
|
+
const s = readConfig(root).sync ?? {};
|
|
412
|
+
if (typeof s.remote === "string" && s.remote.trim()) {
|
|
413
|
+
const prefix = typeof s.prefix === "string" && s.prefix.trim() ? s.prefix.trim() : "harnery";
|
|
414
|
+
return { remote: s.remote.trim(), prefix };
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|