@decibelsystems/tools 3.0.0 → 3.1.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/CHANGELOG.md +66 -0
- package/dist/agentCommands.d.ts +0 -1
- package/dist/agentCommands.d.ts.map +1 -1
- package/dist/agentCommands.js +19 -8
- package/dist/agentCommands.js.map +1 -1
- package/dist/agentPresence.d.ts +5 -3
- package/dist/agentPresence.d.ts.map +1 -1
- package/dist/agentPresence.js +20 -29
- package/dist/agentPresence.js.map +1 -1
- package/dist/architectAdrs.d.ts +13 -1
- package/dist/architectAdrs.d.ts.map +1 -1
- package/dist/architectAdrs.js +83 -1
- package/dist/architectAdrs.js.map +1 -1
- package/dist/artifacts.d.ts +60 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +208 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/asks.d.ts +93 -0
- package/dist/asks.d.ts.map +1 -0
- package/dist/asks.js +383 -0
- package/dist/asks.js.map +1 -0
- package/dist/chat.d.ts +47 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +288 -0
- package/dist/chat.js.map +1 -0
- package/dist/daemon.d.ts +14 -3
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +44 -14
- package/dist/daemon.js.map +1 -1
- package/dist/daemonConfig.d.ts +22 -0
- package/dist/daemonConfig.d.ts.map +1 -1
- package/dist/daemonConfig.js +11 -0
- package/dist/daemonConfig.js.map +1 -1
- package/dist/domain/issue.d.ts +19 -15
- package/dist/domain/issue.d.ts.map +1 -1
- package/dist/domain/issue.js +4 -32
- package/dist/domain/issue.js.map +1 -1
- package/dist/domain/issueCodec.d.ts.map +1 -1
- package/dist/domain/issueCodec.js +10 -0
- package/dist/domain/issueCodec.js.map +1 -1
- package/dist/domain/issueRepository.d.ts +35 -2
- package/dist/domain/issueRepository.d.ts.map +1 -1
- package/dist/domain/issueRepository.js +40 -1
- package/dist/domain/issueRepository.js.map +1 -1
- package/dist/domain/uid.d.ts +16 -0
- package/dist/domain/uid.d.ts.map +1 -0
- package/dist/domain/uid.js +43 -0
- package/dist/domain/uid.js.map +1 -0
- package/dist/facades/definitions.d.ts.map +1 -1
- package/dist/facades/definitions.js +46 -3
- package/dist/facades/definitions.js.map +1 -1
- package/dist/hq/defineAgent.d.ts.map +1 -1
- package/dist/hq/defineAgent.js +7 -11
- package/dist/hq/defineAgent.js.map +1 -1
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +201 -48
- package/dist/httpServer.js.map +1 -1
- package/dist/identity.d.ts +123 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +312 -0
- package/dist/identity.js.map +1 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +24 -1
- package/dist/kernel.js.map +1 -1
- package/dist/killSwitch.d.ts +117 -0
- package/dist/killSwitch.d.ts.map +1 -0
- package/dist/killSwitch.js +161 -0
- package/dist/killSwitch.js.map +1 -0
- package/dist/lib/atomicWrite.d.ts +23 -0
- package/dist/lib/atomicWrite.d.ts.map +1 -1
- package/dist/lib/atomicWrite.js +84 -0
- package/dist/lib/atomicWrite.js.map +1 -1
- package/dist/lib/recordIdAllocator.d.ts +21 -5
- package/dist/lib/recordIdAllocator.d.ts.map +1 -1
- package/dist/lib/recordIdAllocator.js +25 -7
- package/dist/lib/recordIdAllocator.js.map +1 -1
- package/dist/orgId.d.ts +23 -0
- package/dist/orgId.d.ts.map +1 -0
- package/dist/orgId.js +73 -0
- package/dist/orgId.js.map +1 -0
- package/dist/pairCli.d.ts +2 -0
- package/dist/pairCli.d.ts.map +1 -0
- package/dist/pairCli.js +97 -0
- package/dist/pairCli.js.map +1 -0
- package/dist/peerBroker.d.ts +40 -0
- package/dist/peerBroker.d.ts.map +1 -0
- package/dist/peerBroker.js +70 -0
- package/dist/peerBroker.js.map +1 -0
- package/dist/peerBrokerSupervisor.d.ts +21 -0
- package/dist/peerBrokerSupervisor.d.ts.map +1 -0
- package/dist/peerBrokerSupervisor.js +131 -0
- package/dist/peerBrokerSupervisor.js.map +1 -0
- package/dist/projectPaths.d.ts.map +1 -1
- package/dist/projectPaths.js +1 -19
- package/dist/projectPaths.js.map +1 -1
- package/dist/projectRegistry.d.ts +22 -0
- package/dist/projectRegistry.d.ts.map +1 -1
- package/dist/projectRegistry.js +58 -2
- package/dist/projectRegistry.js.map +1 -1
- package/dist/pty/boundary.d.ts +39 -0
- package/dist/pty/boundary.d.ts.map +1 -0
- package/dist/pty/boundary.js +100 -0
- package/dist/pty/boundary.js.map +1 -0
- package/dist/pty/errors.d.ts +6 -0
- package/dist/pty/errors.d.ts.map +1 -0
- package/dist/pty/errors.js +12 -0
- package/dist/pty/errors.js.map +1 -0
- package/dist/pty/fleet.d.ts +159 -0
- package/dist/pty/fleet.d.ts.map +1 -0
- package/dist/pty/fleet.js +543 -0
- package/dist/pty/fleet.js.map +1 -0
- package/dist/pty/launchProfile.d.ts +45 -0
- package/dist/pty/launchProfile.d.ts.map +1 -0
- package/dist/pty/launchProfile.js +149 -0
- package/dist/pty/launchProfile.js.map +1 -0
- package/dist/pty/openTerminal.d.ts +48 -0
- package/dist/pty/openTerminal.d.ts.map +1 -0
- package/dist/pty/openTerminal.js +185 -0
- package/dist/pty/openTerminal.js.map +1 -0
- package/dist/pty/pairDialog.d.ts +16 -0
- package/dist/pty/pairDialog.d.ts.map +1 -0
- package/dist/pty/pairDialog.js +121 -0
- package/dist/pty/pairDialog.js.map +1 -0
- package/dist/pty/pairing.d.ts +86 -0
- package/dist/pty/pairing.d.ts.map +1 -0
- package/dist/pty/pairing.js +351 -0
- package/dist/pty/pairing.js.map +1 -0
- package/dist/pty/routes.d.ts +49 -0
- package/dist/pty/routes.d.ts.map +1 -0
- package/dist/pty/routes.js +624 -0
- package/dist/pty/routes.js.map +1 -0
- package/dist/pty/sessions.d.ts +167 -0
- package/dist/pty/sessions.d.ts.map +1 -0
- package/dist/pty/sessions.js +513 -0
- package/dist/pty/sessions.js.map +1 -0
- package/dist/pty/tmux.d.ts +109 -0
- package/dist/pty/tmux.d.ts.map +1 -0
- package/dist/pty/tmux.js +271 -0
- package/dist/pty/tmux.js.map +1 -0
- package/dist/roster.d.ts +37 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +126 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime/daemonMeta.d.ts +53 -0
- package/dist/runtime/daemonMeta.d.ts.map +1 -0
- package/dist/runtime/daemonMeta.js +121 -0
- package/dist/runtime/daemonMeta.js.map +1 -0
- package/dist/runtime/ensureRuntime.d.ts.map +1 -1
- package/dist/runtime/ensureRuntime.js +5 -10
- package/dist/runtime/ensureRuntime.js.map +1 -1
- package/dist/sentinelIssues.d.ts +16 -0
- package/dist/sentinelIssues.d.ts.map +1 -1
- package/dist/sentinelIssues.js +42 -1
- package/dist/sentinelIssues.js.map +1 -1
- package/dist/server.js +66 -5
- package/dist/server.js.map +1 -1
- package/dist/toolConfig.d.ts.map +1 -1
- package/dist/toolConfig.js +12 -0
- package/dist/toolConfig.js.map +1 -1
- package/dist/tools/agentic/agentQueue.d.ts.map +1 -1
- package/dist/tools/agentic/agentQueue.js +4 -0
- package/dist/tools/agentic/agentQueue.js.map +1 -1
- package/dist/tools/architect/index.d.ts +1 -0
- package/dist/tools/architect/index.d.ts.map +1 -1
- package/dist/tools/architect/index.js +45 -1
- package/dist/tools/architect/index.js.map +1 -1
- package/dist/tools/architect.d.ts.map +1 -1
- package/dist/tools/architect.js +59 -8
- package/dist/tools/architect.js.map +1 -1
- package/dist/tools/artifacts.d.ts +5 -0
- package/dist/tools/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts.js +89 -0
- package/dist/tools/artifacts.js.map +1 -0
- package/dist/tools/asks.d.ts +6 -0
- package/dist/tools/asks.d.ts.map +1 -0
- package/dist/tools/asks.js +104 -0
- package/dist/tools/asks.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +42 -2
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/data-inspector.d.ts +1 -1
- package/dist/tools/data-inspector.d.ts.map +1 -1
- package/dist/tools/data-inspector.js +1 -1
- package/dist/tools/data-inspector.js.map +1 -1
- package/dist/tools/dojo/index.d.ts.map +1 -1
- package/dist/tools/dojo/index.js +52 -3
- package/dist/tools/dojo/index.js.map +1 -1
- package/dist/tools/dojo.d.ts +62 -1
- package/dist/tools/dojo.d.ts.map +1 -1
- package/dist/tools/dojo.js +84 -1
- package/dist/tools/dojo.js.map +1 -1
- package/dist/tools/guardian.d.ts +7 -0
- package/dist/tools/guardian.d.ts.map +1 -1
- package/dist/tools/guardian.js +146 -9
- package/dist/tools/guardian.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/killswitch/index.d.ts +13 -0
- package/dist/tools/killswitch/index.d.ts.map +1 -0
- package/dist/tools/killswitch/index.js +138 -0
- package/dist/tools/killswitch/index.js.map +1 -0
- package/dist/tools/oracle/index.d.ts.map +1 -1
- package/dist/tools/oracle/index.js +8 -0
- package/dist/tools/oracle/index.js.map +1 -1
- package/dist/tools/oracle.d.ts +7 -0
- package/dist/tools/oracle.d.ts.map +1 -1
- package/dist/tools/oracle.js +22 -6
- package/dist/tools/oracle.js.map +1 -1
- package/dist/tools/provenance.d.ts +11 -2
- package/dist/tools/provenance.d.ts.map +1 -1
- package/dist/tools/provenance.js +26 -24
- package/dist/tools/provenance.js.map +1 -1
- package/dist/tools/registry/index.d.ts.map +1 -1
- package/dist/tools/registry/index.js +24 -0
- package/dist/tools/registry/index.js.map +1 -1
- package/dist/tools/sentinel/index.d.ts.map +1 -1
- package/dist/tools/sentinel/index.js +54 -2
- package/dist/tools/sentinel/index.js.map +1 -1
- package/dist/tools/sentinel.d.ts +50 -0
- package/dist/tools/sentinel.d.ts.map +1 -1
- package/dist/tools/sentinel.js +72 -7
- package/dist/tools/sentinel.js.map +1 -1
- package/dist/tools/shared/runTracker.d.ts +2 -0
- package/dist/tools/shared/runTracker.d.ts.map +1 -1
- package/dist/tools/shared/runTracker.js +38 -0
- package/dist/tools/shared/runTracker.js.map +1 -1
- package/dist/tools/swarm.d.ts +3 -1
- package/dist/tools/swarm.d.ts.map +1 -1
- package/dist/tools/swarm.js +6 -12
- package/dist/tools/swarm.js.map +1 -1
- package/dist/tools/voice/index.d.ts +1 -0
- package/dist/tools/voice/index.d.ts.map +1 -1
- package/dist/tools/voice/index.js +33 -2
- package/dist/tools/voice/index.js.map +1 -1
- package/dist/tools/voice.d.ts +48 -1
- package/dist/tools/voice.d.ts.map +1 -1
- package/dist/tools/voice.js +63 -3
- package/dist/tools/voice.js.map +1 -1
- package/dist/transports/bridge.d.ts.map +1 -1
- package/dist/transports/bridge.js +2 -1
- package/dist/transports/bridge.js.map +1 -1
- package/dist/transports/mcp.d.ts.map +1 -1
- package/dist/transports/mcp.js +2 -1
- package/dist/transports/mcp.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +13 -2
- package/templates/AGENT.md +3 -0
- package/templates/CLAUDE-decibel.md +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../../src/pty/tmux.ts"],"names":[],"mappings":"AAmBA,qFAAqF;AACrF,eAAO,MAAM,cAAc,SAAS,CAAC;AAOrC,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,CAAC,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAcvD;AAED,6EAA6E;AAC7E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,QAGmF,CAAC;AAE7G,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAElD;AAwBD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAE/E;AAED,wBAAgB,QAAQ,CAAC,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAetF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAIlF;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAM9F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,YAAY,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,SAAM,GAAG,MAAM,GAAG,SAAS,CAI/E;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,IAAI,EAAE,OAAO,CAAC;CACf;AAgBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKlD;AAgBD,yGAAyG;AACzG,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAG7E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAgB3D;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAY/D;AAED,oEAAoE;AACpE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAEhF"}
|
package/dist/pty/tmux.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// tmux — the substrate hosted sessions live in (ADR-0012 / decibel-hq EPIC-0015)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Every call is execFile with an argv array, never a shell string, so a session
|
|
5
|
+
// name or label can never become a command. Session targets use tmux's `=name`
|
|
6
|
+
// exact-match form: a bare `-t name` is a PREFIX match, so `-t dcb-a` would
|
|
7
|
+
// happily hit `dcb-abc`.
|
|
8
|
+
//
|
|
9
|
+
// Session metadata (project, branch, issue label) is stored ON the tmux session
|
|
10
|
+
// as user options (@dcb_*), not in a daemon-side file. It then lives exactly as
|
|
11
|
+
// long as the session does and survives a daemon restart with nothing to
|
|
12
|
+
// reconcile.
|
|
13
|
+
// ============================================================================
|
|
14
|
+
import { execFile } from 'child_process';
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
import { log } from '../config.js';
|
|
17
|
+
import { SessionError } from './errors.js';
|
|
18
|
+
/** Prefix for sessions the daemon spawned. Anything else is adopted, never owned. */
|
|
19
|
+
export const SPAWNED_PREFIX = 'dcb-';
|
|
20
|
+
/**
|
|
21
|
+
* launchd does not give the daemon a login PATH, so `tmux` alone may not
|
|
22
|
+
* resolve. Probe the usual install locations first.
|
|
23
|
+
*/
|
|
24
|
+
let tmuxPath = null;
|
|
25
|
+
export function resolveTmux() {
|
|
26
|
+
if (tmuxPath)
|
|
27
|
+
return tmuxPath;
|
|
28
|
+
for (const p of ['/opt/homebrew/bin/tmux', '/usr/local/bin/tmux', '/usr/bin/tmux']) {
|
|
29
|
+
if (existsSync(p))
|
|
30
|
+
return (tmuxPath = p);
|
|
31
|
+
}
|
|
32
|
+
return (tmuxPath = 'tmux');
|
|
33
|
+
}
|
|
34
|
+
/** Test seam: forget the cached binary path. */
|
|
35
|
+
export function _resetTmuxPath(p = null) {
|
|
36
|
+
tmuxPath = p;
|
|
37
|
+
}
|
|
38
|
+
export const OWNED_SOCKET_DEFAULT = 'decibel';
|
|
39
|
+
export function socketArgs(server) {
|
|
40
|
+
// -f /dev/null: the owned server must not read ~/.tmux.conf. HQ's pane needs
|
|
41
|
+
// predictable behaviour — no custom prefix, and no `mouse on`, whose mouse
|
|
42
|
+
// capture breaks xterm.js selection. (-f only matters when the server starts;
|
|
43
|
+
// passing it on every call is harmless.)
|
|
44
|
+
//
|
|
45
|
+
// -u on every call: launchd gives the daemon no locale, and a tmux client
|
|
46
|
+
// without one is treated as non-UTF-8 — every non-ASCII glyph Claude draws
|
|
47
|
+
// (❯, ⏵⏵, the logo) arrived in HQ as '_'. cleanEnv also supplies a UTF-8
|
|
48
|
+
// LANG/LC_CTYPE, so a server started by the daemon is UTF-8 too. Found by
|
|
49
|
+
// decibel-hq on its first live pass.
|
|
50
|
+
if (server === 'owned')
|
|
51
|
+
return ['-u', '-L', process.env.DECIBEL_TMUX_SOCKET || OWNED_SOCKET_DEFAULT, '-f', '/dev/null'];
|
|
52
|
+
const user = process.env.DECIBEL_TMUX_USER_SOCKET;
|
|
53
|
+
return user ? ['-u', '-L', user] : ['-u'];
|
|
54
|
+
}
|
|
55
|
+
/** Which server a session key lives on: the prefix is the ownership mark. */
|
|
56
|
+
/**
|
|
57
|
+
* Terminal title for clients of the owned server (set-titles): the repo, plus
|
|
58
|
+
* the issue label, or else a branch other than main/master. Built from the
|
|
59
|
+
* @dcb_* options the daemon sets at spawn, which cleanLabel has already
|
|
60
|
+
* stripped of control characters. tmux sends it as OSC 2 to every attached
|
|
61
|
+
* client, so a Ghostty tab is titled without any title text in its command
|
|
62
|
+
* (ISS-0210).
|
|
63
|
+
*/
|
|
64
|
+
export const TITLE_FORMAT = '#{?@dcb_project,#{@dcb_project},#{session_name}}' +
|
|
65
|
+
'#{?@dcb_issue, · #{@dcb_issue},' +
|
|
66
|
+
'#{?#{&&:#{@dcb_branch},#{&&:#{!=:#{@dcb_branch},main},#{!=:#{@dcb_branch},master}}}, · #{@dcb_branch},}}';
|
|
67
|
+
export function serverFor(name) {
|
|
68
|
+
return name.startsWith(SPAWNED_PREFIX) ? 'owned' : 'user';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The environment every tmux client and spawned claude gets. TMUX is removed so
|
|
72
|
+
* a daemon started inside tmux does not make nested-client calls. CLAUDECODE,
|
|
73
|
+
* CLAUDE_CODE_* and CLAUDE_PID are removed because claude refuses to start when
|
|
74
|
+
* it sees them (the nested-session guard): a daemon ever launched from inside a
|
|
75
|
+
* Claude Code session would otherwise start an owned server whose every session
|
|
76
|
+
* dies instantly. Found by decibel-hq against its mock of this contract.
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* The daemon's own environment, which a spawned agent must not inherit
|
|
80
|
+
* (ISS-0225). launchd starts the daemon with its env file loaded and
|
|
81
|
+
* NODE_ENV=production, and before this every hosted Claude session held them
|
|
82
|
+
* all, including SUPABASE_SERVICE_KEY, which bypasses RLS on Core. Only the
|
|
83
|
+
* daemon writes with service_role; an agent and anything it runs must not.
|
|
84
|
+
* NODE_ENV=production also makes npm/pnpm skip and prune devDependencies.
|
|
85
|
+
*
|
|
86
|
+
* Matched by name: credentials by suffix, plus the daemon-only config.
|
|
87
|
+
* ANTHROPIC_* is kept, so Claude Code's own auth is untouched.
|
|
88
|
+
*/
|
|
89
|
+
const DAEMON_ONLY = new Set(['NODE_ENV', 'SUPABASE_URL', 'SUPABASE_ANON_KEY', 'SUPABASE_SERVICE_KEY', 'SUPABASE_SERVICE_ROLE_KEY', 'DECIBEL_ORG_ID']);
|
|
90
|
+
const CREDENTIAL_RE = /(_KEY|_TOKEN|_SECRET|PASSWORD|_DATABASE_URL|_DSN)$/i;
|
|
91
|
+
export function isDaemonOnlyVar(name) {
|
|
92
|
+
if (name.startsWith('ANTHROPIC_'))
|
|
93
|
+
return false;
|
|
94
|
+
return DAEMON_ONLY.has(name) || CREDENTIAL_RE.test(name);
|
|
95
|
+
}
|
|
96
|
+
/** Names in `base` that cleanEnv drops for being the daemon's. Names only, never values. */
|
|
97
|
+
export function daemonOnlyNames(base = process.env) {
|
|
98
|
+
return Object.keys(base).filter((k) => base[k] !== undefined && isDaemonOnlyVar(k)).sort();
|
|
99
|
+
}
|
|
100
|
+
export function cleanEnv(base = process.env) {
|
|
101
|
+
const env = {};
|
|
102
|
+
for (const [k, v] of Object.entries(base)) {
|
|
103
|
+
if (v === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (k === 'TMUX' || k === 'TMUX_PANE' || k === 'CLAUDECODE' || k === 'CLAUDE_PID' || k.startsWith('CLAUDE_CODE_'))
|
|
106
|
+
continue;
|
|
107
|
+
if (isDaemonOnlyVar(k))
|
|
108
|
+
continue;
|
|
109
|
+
env[k] = v;
|
|
110
|
+
}
|
|
111
|
+
// No locale at all (launchd) means tmux and claude both fall back to ASCII.
|
|
112
|
+
// Supply UTF-8 only when nothing is set, so a user's own choice stands.
|
|
113
|
+
if (!env.LANG && !env.LC_ALL && !env.LC_CTYPE) {
|
|
114
|
+
env.LANG = 'en_US.UTF-8';
|
|
115
|
+
env.LC_CTYPE = 'en_US.UTF-8';
|
|
116
|
+
}
|
|
117
|
+
return env;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* tmux keeps the environment its server started with as the GLOBAL
|
|
121
|
+
* environment and copies it into every new session. A server started before
|
|
122
|
+
* ISS-0225 already holds the daemon's variables, so cleaning the client env
|
|
123
|
+
* is not enough: unset them there too. Applied before every spawn and at
|
|
124
|
+
* daemon start.
|
|
125
|
+
*/
|
|
126
|
+
export function unsetDaemonEnvArgs(base = process.env) {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const name of daemonOnlyNames(base))
|
|
129
|
+
out.push(';', 'set-environment', '-g', '-u', name);
|
|
130
|
+
return out.slice(1);
|
|
131
|
+
}
|
|
132
|
+
export function tmux(server, args, timeoutMs = 5000) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
execFile(resolveTmux(), [...socketArgs(server), ...args], { timeout: timeoutMs, env: cleanEnv() }, (err, stdout, stderr) => {
|
|
135
|
+
resolve({ ok: !err, stdout: String(stdout), stderr: String(stderr) || (err ? err.message : '') });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Exact-match session target, for commands that take a SESSION target:
|
|
141
|
+
* has-session, kill-session, attach-session.
|
|
142
|
+
*/
|
|
143
|
+
export function sessionTarget(name) {
|
|
144
|
+
return `=${name}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Exact-match target for everything that parses -t as a pane or window:
|
|
148
|
+
* set-option, show-options, send-keys, respawn-pane. tmux 3.5a rejects a bare
|
|
149
|
+
* `=name` there with "no such session"; the trailing colon is required
|
|
150
|
+
* (verified 2026-09-25, and hit independently by decibel-hq's mock).
|
|
151
|
+
*/
|
|
152
|
+
export function paneTarget(name) {
|
|
153
|
+
return `=${name}:`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Field separator for -F output. PRINTABLE on purpose: with no LANG/LC_* in
|
|
157
|
+
* the environment — which is exactly the launchd daemon's environment — tmux
|
|
158
|
+
* rewrites a TAB in format output as '_', every field collapsed into the
|
|
159
|
+
* session name, and no spawned session ever matched its project. The tests
|
|
160
|
+
* passed because the shell running them had a locale. Pinned by a test that
|
|
161
|
+
* lists with the locale removed.
|
|
162
|
+
*/
|
|
163
|
+
export const FIELD_SEP = '|:dcb:|';
|
|
164
|
+
/**
|
|
165
|
+
* Newlines and the field separator would corrupt list output, and a label has
|
|
166
|
+
* no business being long. Applied to every value written into a tmux option.
|
|
167
|
+
*/
|
|
168
|
+
export function cleanLabel(v, max = 200) {
|
|
169
|
+
if (v === undefined || v === null)
|
|
170
|
+
return undefined;
|
|
171
|
+
const cleaned = String(v).split(FIELD_SEP).join(' ').replace(/[\t\r\n]/g, ' ').trim().slice(0, max);
|
|
172
|
+
return cleaned || undefined;
|
|
173
|
+
}
|
|
174
|
+
const LIST_FORMAT = [
|
|
175
|
+
'#{session_name}',
|
|
176
|
+
'#{@dcb_project}',
|
|
177
|
+
'#{@dcb_branch}',
|
|
178
|
+
'#{@dcb_issue}',
|
|
179
|
+
'#{@dcb_uuid}',
|
|
180
|
+
'#{session_created}',
|
|
181
|
+
'#{pane_current_path}',
|
|
182
|
+
'#{pane_dead}',
|
|
183
|
+
// #{pane_current_path} is empty once the pane is dead, so spawned sessions
|
|
184
|
+
// also record their cwd as an option (found by decibel-hq's mock).
|
|
185
|
+
'#{@dcb_cwd}',
|
|
186
|
+
].join(FIELD_SEP);
|
|
187
|
+
/**
|
|
188
|
+
* "No server" is a normal answer; "could not ask" is not. A missing socket
|
|
189
|
+
* means no server, so no sessions. Any other failure — a server that exists
|
|
190
|
+
* but did not answer — must NOT read as an empty list, because HQ would render
|
|
191
|
+
* a live session as gone. It gets one retry and then throws. (A spawned session
|
|
192
|
+
* once went unlisted for minutes while its server was plainly up; the old check
|
|
193
|
+
* swallowed every "error connecting to" as "no server". Cause unverified.)
|
|
194
|
+
*/
|
|
195
|
+
export function isNoServer(stderr) {
|
|
196
|
+
// A stale socket file (the server died without removing it) refuses the
|
|
197
|
+
// connection; that is also "no server".
|
|
198
|
+
return /no server running/.test(stderr) ||
|
|
199
|
+
/error connecting to .*\((No such file or directory|Connection refused)\)/.test(stderr);
|
|
200
|
+
}
|
|
201
|
+
async function tmuxRead(server, args) {
|
|
202
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
203
|
+
const r = await tmux(server, args);
|
|
204
|
+
if (r.ok)
|
|
205
|
+
return r;
|
|
206
|
+
if (isNoServer(r.stderr))
|
|
207
|
+
return null;
|
|
208
|
+
log(`Sessions: tmux ${args[0]} (${server}) failed${attempt ? '' : ', retrying'}: ${r.stderr.trim() || 'no stderr'}`);
|
|
209
|
+
if (!attempt)
|
|
210
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
211
|
+
else {
|
|
212
|
+
throw new SessionError(503, 'TMUX_UNAVAILABLE', `The ${server} tmux server did not answer: ${r.stderr.trim() || 'no stderr'}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
/** Every session on one server. Empty when that server is not running; throws when it cannot be read. */
|
|
218
|
+
export async function listSessions(server) {
|
|
219
|
+
const r = await tmuxRead(server, ['list-sessions', '-F', LIST_FORMAT]);
|
|
220
|
+
return r ? parseSessions(r.stdout) : [];
|
|
221
|
+
}
|
|
222
|
+
export function parseSessions(stdout) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const line of stdout.split('\n')) {
|
|
225
|
+
if (!line.trim())
|
|
226
|
+
continue;
|
|
227
|
+
const [name, project, branch, issue, uuid, created, paneCwd, dead, dcbCwd] = line.split(FIELD_SEP);
|
|
228
|
+
const cwd = dcbCwd || paneCwd;
|
|
229
|
+
const s = { name, dead: dead === '1' };
|
|
230
|
+
if (uuid)
|
|
231
|
+
s.claude_session_id = uuid;
|
|
232
|
+
if (project)
|
|
233
|
+
s.project_id = project;
|
|
234
|
+
if (branch)
|
|
235
|
+
s.branch = branch;
|
|
236
|
+
if (issue)
|
|
237
|
+
s.issue_label = issue;
|
|
238
|
+
if (created && /^\d+$/.test(created))
|
|
239
|
+
s.created = new Date(Number(created) * 1000).toISOString();
|
|
240
|
+
if (cwd)
|
|
241
|
+
s.cwd = cwd;
|
|
242
|
+
out.push(s);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* pane tty (without /dev/) → session name, across BOTH servers, for matching
|
|
248
|
+
* claude-peers ttys. A user-server session carrying the dcb- prefix is skipped:
|
|
249
|
+
* the prefix means "owned", and honouring it on the wrong server would route
|
|
250
|
+
* attach and kill to a session the daemon does not own.
|
|
251
|
+
*/
|
|
252
|
+
export async function paneTtyMap() {
|
|
253
|
+
const m = new Map();
|
|
254
|
+
for (const server of ['owned', 'user']) {
|
|
255
|
+
const r = await tmuxRead(server, ['list-panes', '-a', '-F', `#{pane_tty}${FIELD_SEP}#{session_name}`]);
|
|
256
|
+
if (!r)
|
|
257
|
+
continue;
|
|
258
|
+
for (const line of r.stdout.split('\n')) {
|
|
259
|
+
const [tty, name] = line.split(FIELD_SEP);
|
|
260
|
+
if (!tty || !name || serverFor(name) !== server)
|
|
261
|
+
continue;
|
|
262
|
+
m.set(tty.replace(/^\/dev\//, ''), name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return m;
|
|
266
|
+
}
|
|
267
|
+
/** Find a session by key on the server its key says it lives on. */
|
|
268
|
+
export async function findSession(name) {
|
|
269
|
+
return (await listSessions(serverFor(name))).find((s) => s.name === name);
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=tmux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux.js","sourceRoot":"","sources":["../../src/pty/tmux.ts"],"names":[],"mappings":"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"}
|
package/dist/roster.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ScopeFilter {
|
|
2
|
+
assignee?: unknown;
|
|
3
|
+
team?: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface ScopeTerm {
|
|
6
|
+
requested: string;
|
|
7
|
+
/** Every id a record may carry to match this filter. */
|
|
8
|
+
match: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedScope {
|
|
11
|
+
assignee?: ScopeTerm;
|
|
12
|
+
team?: ScopeTerm;
|
|
13
|
+
/** True when the roster answered and aliases were applied. */
|
|
14
|
+
expanded: boolean;
|
|
15
|
+
/** Present whenever the result may be short, and why. */
|
|
16
|
+
note?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RosterRow {
|
|
19
|
+
id: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
canonical_id: string | null;
|
|
22
|
+
aliases: string[] | null;
|
|
23
|
+
}
|
|
24
|
+
export type RosterResolver = (ids: string[]) => Promise<RosterRow[]>;
|
|
25
|
+
/** Test seam. */
|
|
26
|
+
export declare function _setRosterResolver(fn: RosterResolver | null): void;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a scope filter. Returns null when no filter was given, so an
|
|
29
|
+
* unscoped call keeps exactly its current behaviour.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveScope(f: ScopeFilter): Promise<ResolvedScope | null>;
|
|
32
|
+
/** Does a record fall inside the scope? Both terms must hold when both are set. */
|
|
33
|
+
export declare function inScope(record: {
|
|
34
|
+
assignee?: string;
|
|
35
|
+
team?: string;
|
|
36
|
+
}, scope: ResolvedScope): boolean;
|
|
37
|
+
//# sourceMappingURL=roster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster.d.ts","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8DAA8D;IAC9D,QAAQ,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;AAkBrE,iBAAiB;AACjB,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAGlE;AA+BD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAoChF;AAED,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAInG"}
|
package/dist/roster.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Roster resolution for scoped reads (EPIC-0040 slice 3)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// A filter such as assignee=user:<ben> has to match every id that means Ben:
|
|
5
|
+
// the canonical user:<uuid> AND every human:<name> claimed to that account.
|
|
6
|
+
// Records are never rewritten to upgrade a human: to a user:, so without this
|
|
7
|
+
// expansion a filter by user:<uuid> misses every record written before the
|
|
8
|
+
// claim (44 human:ben records in this repo alone) and returns a confidently
|
|
9
|
+
// short list.
|
|
10
|
+
//
|
|
11
|
+
// HQ owns the mapping: hq.resolve_ids(p_org, p_ids) returns, per input id, its
|
|
12
|
+
// canonical_id and aliases. The daemon turns one filter id into an IN-list:
|
|
13
|
+
// {requested} ∪ {canonical_id} ∪ aliases.
|
|
14
|
+
//
|
|
15
|
+
// Offline degradation, agreed with decibel-hq: when the roster is unreachable,
|
|
16
|
+
// match the literal id only and SAY SO in the result ("not expanded"). Never a
|
|
17
|
+
// guess, never a quiet omission.
|
|
18
|
+
// ============================================================================
|
|
19
|
+
import { assertActor, assertTeam, IdentityError } from './identity.js';
|
|
20
|
+
import { requireOrgId } from './orgId.js';
|
|
21
|
+
import { getSupabaseServiceClient, isSupabaseConfigured } from './lib/supabase.js';
|
|
22
|
+
const CACHE_MS = 5 * 60_000;
|
|
23
|
+
const TIMEOUT_MS = 3_000;
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
async function supabaseResolver(ids) {
|
|
26
|
+
if (!isSupabaseConfigured())
|
|
27
|
+
throw new Error('Supabase is not configured on this process');
|
|
28
|
+
const client = getSupabaseServiceClient();
|
|
29
|
+
const call = client.schema('hq').rpc('resolve_ids', { p_org: requireOrgId(), p_ids: ids });
|
|
30
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`no answer in ${TIMEOUT_MS}ms`)), TIMEOUT_MS));
|
|
31
|
+
const { data, error } = (await Promise.race([call, timeout]));
|
|
32
|
+
if (error)
|
|
33
|
+
throw new Error(error.message);
|
|
34
|
+
if (!Array.isArray(data))
|
|
35
|
+
throw new Error('resolve_ids returned no rows');
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
let resolver = supabaseResolver;
|
|
39
|
+
/** Test seam. */
|
|
40
|
+
export function _setRosterResolver(fn) {
|
|
41
|
+
resolver = fn ?? supabaseResolver;
|
|
42
|
+
cache.clear();
|
|
43
|
+
}
|
|
44
|
+
/** Validate a filter's SHAPE. Membership is HQ's business, never checked here. */
|
|
45
|
+
function parseFilter(f) {
|
|
46
|
+
const out = {};
|
|
47
|
+
try {
|
|
48
|
+
if (f.assignee !== undefined && f.assignee !== '') {
|
|
49
|
+
if (typeof f.assignee !== 'string')
|
|
50
|
+
throw new IdentityError('assignee must be a string');
|
|
51
|
+
assertActor(f.assignee);
|
|
52
|
+
out.assignee = f.assignee;
|
|
53
|
+
}
|
|
54
|
+
if (f.team !== undefined && f.team !== '') {
|
|
55
|
+
if (typeof f.team !== 'string')
|
|
56
|
+
throw new IdentityError('team must be a string');
|
|
57
|
+
assertTeam(f.team);
|
|
58
|
+
out.team = f.team;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new Error(`Invalid scope filter: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function termFrom(requested, row) {
|
|
67
|
+
const ids = new Set([requested]);
|
|
68
|
+
if (row && row.kind !== 'unresolved') {
|
|
69
|
+
if (row.canonical_id)
|
|
70
|
+
ids.add(row.canonical_id);
|
|
71
|
+
for (const a of row.aliases ?? [])
|
|
72
|
+
ids.add(a);
|
|
73
|
+
}
|
|
74
|
+
return { requested, match: [...ids] };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a scope filter. Returns null when no filter was given, so an
|
|
78
|
+
* unscoped call keeps exactly its current behaviour.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveScope(f) {
|
|
81
|
+
const parsed = parseFilter(f);
|
|
82
|
+
const wanted = [parsed.assignee, parsed.team].filter((x) => !!x);
|
|
83
|
+
if (!wanted.length)
|
|
84
|
+
return null;
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const rows = new Map();
|
|
87
|
+
const missing = wanted.filter((id) => {
|
|
88
|
+
const hit = cache.get(id);
|
|
89
|
+
if (hit && now - hit.at < CACHE_MS) {
|
|
90
|
+
rows.set(id, hit.row);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
});
|
|
95
|
+
let failure;
|
|
96
|
+
if (missing.length) {
|
|
97
|
+
try {
|
|
98
|
+
const answered = await resolver(missing);
|
|
99
|
+
for (const row of answered) {
|
|
100
|
+
rows.set(row.id, row);
|
|
101
|
+
cache.set(row.id, { row, at: now });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const scope = { expanded: !failure };
|
|
109
|
+
if (parsed.assignee)
|
|
110
|
+
scope.assignee = termFrom(parsed.assignee, rows.get(parsed.assignee));
|
|
111
|
+
if (parsed.team)
|
|
112
|
+
scope.team = termFrom(parsed.team, rows.get(parsed.team));
|
|
113
|
+
if (failure) {
|
|
114
|
+
scope.note = `Roster unreachable (${failure}): matched the literal id only; local identities were not expanded, so this list may be short.`;
|
|
115
|
+
}
|
|
116
|
+
return scope;
|
|
117
|
+
}
|
|
118
|
+
/** Does a record fall inside the scope? Both terms must hold when both are set. */
|
|
119
|
+
export function inScope(record, scope) {
|
|
120
|
+
if (scope.assignee && !(record.assignee && scope.assignee.match.includes(record.assignee)))
|
|
121
|
+
return false;
|
|
122
|
+
if (scope.team && !(record.team && scope.team.match.includes(record.team)))
|
|
123
|
+
return false;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=roster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster.js","sourceRoot":"","sources":["../src/roster.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved per call rather than captured at import, the way daemonConfig does:
|
|
3
|
+
* os.homedir() tracks $HOME, and a module-level constant freezes whatever it
|
|
4
|
+
* was when the file was first loaded — which makes this unverifiable in a test
|
|
5
|
+
* and wrong in any process that changes HOME.
|
|
6
|
+
*/
|
|
7
|
+
export declare function decibelHome(): string;
|
|
8
|
+
/**
|
|
9
|
+
* DECIBEL_DAEMON_SIDECAR=<dir> marks a second daemon run beside the real one
|
|
10
|
+
* (a branch build on another port, decibel-hq ADR-0018 testing). Its pid,
|
|
11
|
+
* meta and logs live in <dir>, so it never takes over the machine's
|
|
12
|
+
* advertisement, and server.ts skips the machine singletons (peer broker,
|
|
13
|
+
* presence, HQ command dispatch, chat). Config, env and projects stay shared.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sidecarDir(): string | null;
|
|
16
|
+
/** Where this daemon's pid, meta and logs live: the sidecar dir, else ~/.decibel. */
|
|
17
|
+
export declare function daemonStateDir(): string;
|
|
18
|
+
export declare function metaPath(): string;
|
|
19
|
+
/** The port the daemon binds when nothing says otherwise. */
|
|
20
|
+
export declare const DEFAULT_DAEMON_PORT = 4888;
|
|
21
|
+
export interface DaemonMeta {
|
|
22
|
+
started_at: string;
|
|
23
|
+
crash_count: number;
|
|
24
|
+
port?: number;
|
|
25
|
+
pid?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Is this pid a process that currently exists?
|
|
29
|
+
*
|
|
30
|
+
* Signal 0 performs the permission and existence checks without delivering
|
|
31
|
+
* anything. EPERM means the process is there and owned by someone else, which
|
|
32
|
+
* is still alive — only ESRCH means gone.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isPidAlive(pid: number | undefined): boolean;
|
|
35
|
+
export declare function readDaemonMeta(): DaemonMeta | null;
|
|
36
|
+
/**
|
|
37
|
+
* The advertised port, or undefined when there is no usable advertisement.
|
|
38
|
+
*
|
|
39
|
+
* An entry whose pid is dead is treated as absent rather than followed. That is
|
|
40
|
+
* the whole point: a stale port is worse than no port, because callers fall
|
|
41
|
+
* back to the default when there is nothing to read and dial a corpse when
|
|
42
|
+
* there is.
|
|
43
|
+
*/
|
|
44
|
+
export declare function advertisedPort(): number | undefined;
|
|
45
|
+
/** Discovery order shared by every client: explicit > env > meta > default. */
|
|
46
|
+
export declare function resolveDaemonPort(explicit?: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* True when someone else is currently advertising — a live pid that is not us.
|
|
49
|
+
* Taking discovery from a running daemon is how a second instance silently
|
|
50
|
+
* breaks the first one's clients, so callers refuse rather than overwrite.
|
|
51
|
+
*/
|
|
52
|
+
export declare function heldByAnotherProcess(): DaemonMeta | null;
|
|
53
|
+
//# sourceMappingURL=daemonMeta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemonMeta.d.ts","sourceRoot":"","sources":["../../src/runtime/daemonMeta.ts"],"names":[],"mappings":"AA6BA;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAG1C;AAED,qFAAqF;AACrF,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQ3D;AAED,wBAAgB,cAAc,IAAI,UAAU,GAAG,IAAI,CAOlD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAKnD;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,GAAG,IAAI,CAIxD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// daemon.meta — machine-global daemon discovery
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// ~/.decibel/daemon.meta is how every client on the machine finds the daemon:
|
|
6
|
+
// the session-init hook, the issue-close hook, the HQ client, ensureRuntime,
|
|
7
|
+
// the CLI. One small JSON file that three modules used to parse for themselves.
|
|
8
|
+
//
|
|
9
|
+
// ISS-0179. Two defects, and the second is the worse one:
|
|
10
|
+
//
|
|
11
|
+
// 1. ANY `--http` run advertised itself here, not just `--daemon`. Starting a
|
|
12
|
+
// throwaway server on another port — a test, a second checkout, a port
|
|
13
|
+
// conflict, a demo — silently redirected every hook on the machine to it.
|
|
14
|
+
//
|
|
15
|
+
// 2. Nothing removed a stale entry and no reader checked one. When that
|
|
16
|
+
// throwaway process died, the entry stayed, and every hook kept dialling a
|
|
17
|
+
// port with nothing behind it. The failure was silent at every layer: curl
|
|
18
|
+
// got connection-refused, the hook read empty output as "nothing to do",
|
|
19
|
+
// and the user saw a successful commit.
|
|
20
|
+
//
|
|
21
|
+
// The entry records the pid that wrote it, which is enough to tell a live
|
|
22
|
+
// advertisement from a dead one. That check lives here, once, because the
|
|
23
|
+
// reason it was missing is that it would have had to be written three times.
|
|
24
|
+
// ============================================================================
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import os from 'os';
|
|
28
|
+
/**
|
|
29
|
+
* Resolved per call rather than captured at import, the way daemonConfig does:
|
|
30
|
+
* os.homedir() tracks $HOME, and a module-level constant freezes whatever it
|
|
31
|
+
* was when the file was first loaded — which makes this unverifiable in a test
|
|
32
|
+
* and wrong in any process that changes HOME.
|
|
33
|
+
*/
|
|
34
|
+
export function decibelHome() {
|
|
35
|
+
return path.join(os.homedir(), '.decibel');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* DECIBEL_DAEMON_SIDECAR=<dir> marks a second daemon run beside the real one
|
|
39
|
+
* (a branch build on another port, decibel-hq ADR-0018 testing). Its pid,
|
|
40
|
+
* meta and logs live in <dir>, so it never takes over the machine's
|
|
41
|
+
* advertisement, and server.ts skips the machine singletons (peer broker,
|
|
42
|
+
* presence, HQ command dispatch, chat). Config, env and projects stay shared.
|
|
43
|
+
*/
|
|
44
|
+
export function sidecarDir() {
|
|
45
|
+
const dir = process.env.DECIBEL_DAEMON_SIDECAR;
|
|
46
|
+
return dir ? path.resolve(dir) : null;
|
|
47
|
+
}
|
|
48
|
+
/** Where this daemon's pid, meta and logs live: the sidecar dir, else ~/.decibel. */
|
|
49
|
+
export function daemonStateDir() {
|
|
50
|
+
return sidecarDir() ?? decibelHome();
|
|
51
|
+
}
|
|
52
|
+
export function metaPath() {
|
|
53
|
+
return path.join(daemonStateDir(), 'daemon.meta');
|
|
54
|
+
}
|
|
55
|
+
/** The port the daemon binds when nothing says otherwise. */
|
|
56
|
+
export const DEFAULT_DAEMON_PORT = 4888;
|
|
57
|
+
/**
|
|
58
|
+
* Is this pid a process that currently exists?
|
|
59
|
+
*
|
|
60
|
+
* Signal 0 performs the permission and existence checks without delivering
|
|
61
|
+
* anything. EPERM means the process is there and owned by someone else, which
|
|
62
|
+
* is still alive — only ESRCH means gone.
|
|
63
|
+
*/
|
|
64
|
+
export function isPidAlive(pid) {
|
|
65
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return err.code === 'EPERM';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function readDaemonMeta() {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(fs.readFileSync(metaPath(), 'utf-8'));
|
|
78
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The advertised port, or undefined when there is no usable advertisement.
|
|
86
|
+
*
|
|
87
|
+
* An entry whose pid is dead is treated as absent rather than followed. That is
|
|
88
|
+
* the whole point: a stale port is worse than no port, because callers fall
|
|
89
|
+
* back to the default when there is nothing to read and dial a corpse when
|
|
90
|
+
* there is.
|
|
91
|
+
*/
|
|
92
|
+
export function advertisedPort() {
|
|
93
|
+
const meta = readDaemonMeta();
|
|
94
|
+
if (!meta || !Number.isInteger(meta.port))
|
|
95
|
+
return undefined;
|
|
96
|
+
if (meta.pid !== undefined && !isPidAlive(meta.pid))
|
|
97
|
+
return undefined;
|
|
98
|
+
return meta.port;
|
|
99
|
+
}
|
|
100
|
+
/** Discovery order shared by every client: explicit > env > meta > default. */
|
|
101
|
+
export function resolveDaemonPort(explicit) {
|
|
102
|
+
if (Number.isInteger(explicit) && explicit > 0 && explicit < 65536) {
|
|
103
|
+
return explicit;
|
|
104
|
+
}
|
|
105
|
+
const fromEnv = Number(process.env.DECIBEL_DAEMON_PORT);
|
|
106
|
+
if (Number.isInteger(fromEnv) && fromEnv > 0 && fromEnv < 65536)
|
|
107
|
+
return fromEnv;
|
|
108
|
+
return advertisedPort() ?? DEFAULT_DAEMON_PORT;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* True when someone else is currently advertising — a live pid that is not us.
|
|
112
|
+
* Taking discovery from a running daemon is how a second instance silently
|
|
113
|
+
* breaks the first one's clients, so callers refuse rather than overwrite.
|
|
114
|
+
*/
|
|
115
|
+
export function heldByAnotherProcess() {
|
|
116
|
+
const meta = readDaemonMeta();
|
|
117
|
+
if (!meta || meta.pid === undefined || meta.pid === process.pid)
|
|
118
|
+
return null;
|
|
119
|
+
return isPidAlive(meta.pid) ? meta : null;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=daemonMeta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemonMeta.js","sourceRoot":"","sources":["../../src/runtime/daemonMeta.ts"],"names":[],"mappings":"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"}
|