@hwip/cli 0.1.1 → 0.1.3
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 +21 -1
- package/dist/answer-files.d.ts +39 -0
- package/dist/answer-files.js +35 -0
- package/dist/ask-seal.d.ts +5 -1
- package/dist/ask-seal.js +4 -1
- package/dist/ask.d.ts +13 -0
- package/dist/ask.js +37 -6
- package/dist/config.d.ts +14 -0
- package/dist/config.js +23 -1
- package/dist/listener.d.ts +60 -2
- package/dist/listener.js +185 -30
- package/dist/remote-agents.d.ts +22 -0
- package/dist/remote-agents.js +22 -5
- package/dist/run-seqs.d.ts +28 -0
- package/dist/run-seqs.js +79 -0
- package/dist/session-registry.d.ts +56 -4
- package/dist/session-registry.js +93 -14
- package/dist/templates.js +16 -5
- package/dist/tmux-liveness.d.ts +7 -0
- package/dist/tmux-liveness.js +22 -1
- package/dist/turn-recorder-wire.js +1 -1
- package/dist/turn-recorder.d.ts +8 -0
- package/dist/turn-recorder.js +10 -2
- package/dist/wrapper.d.ts +12 -4
- package/dist/wrapper.js +47 -6
- package/package.json +16 -2
|
@@ -23,6 +23,26 @@ export interface KnownSession {
|
|
|
23
23
|
agentKind: string;
|
|
24
24
|
project?: string;
|
|
25
25
|
label?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Which run of the name this was (hwip docs/session-runs.md): tmux
|
|
28
|
+
* `session_created` in epoch seconds, as a decimal string. Absent from
|
|
29
|
+
* entries written before runs were recorded.
|
|
30
|
+
*/
|
|
31
|
+
runId?: string;
|
|
32
|
+
/** When that run started (ISO) — the same instant as `runId`. */
|
|
33
|
+
startedAt?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The agent's own id for the conversation this run last had, as the sweep
|
|
36
|
+
* read it off the live pane — what a resume picks back up. Absent for an
|
|
37
|
+
* agent whose id this machine cannot read, and in files written before it.
|
|
38
|
+
*/
|
|
39
|
+
agentSessionId?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The later tmux sessions this run went on in (`session_created`, as
|
|
42
|
+
* `runId`): the same conversation carried on after an `exit` is the same
|
|
43
|
+
* run (`continuedRunId`). Absent for a run that has had one tmux session.
|
|
44
|
+
*/
|
|
45
|
+
tmuxRunIds?: string[];
|
|
26
46
|
/** Last time this machine saw the session running. */
|
|
27
47
|
lastSeenAt: string;
|
|
28
48
|
}
|
|
@@ -33,12 +53,14 @@ export interface KnownSession {
|
|
|
33
53
|
* that has run agents for years.
|
|
34
54
|
*/
|
|
35
55
|
export declare const MAX_KNOWN_SESSIONS = 100;
|
|
56
|
+
/** Runs kept per name: a slot reused all day would otherwise fill the file with one project. */
|
|
57
|
+
export declare const MAX_RUNS_PER_NAME = 10;
|
|
36
58
|
/** Forgotten after this long unseen — a directory from months ago is more
|
|
37
59
|
* likely to have moved than to be what the user meant. */
|
|
38
60
|
export declare const KNOWN_SESSION_TTL_MS: number;
|
|
39
61
|
/**
|
|
40
|
-
* What makes two sightings the same remembered run: the tmux name
|
|
41
|
-
*
|
|
62
|
+
* What makes two sightings the same remembered run: the tmux name, the agent
|
|
63
|
+
* that was running under it, AND the tmux session it ran in (`runId`).
|
|
42
64
|
*
|
|
43
65
|
* Keyed by name alone, a `hwip cc` in a project erased whatever pi had done in
|
|
44
66
|
* the same slot — the ended run left no row to resume and no row to read, and
|
|
@@ -48,8 +70,13 @@ export declare const KNOWN_SESSION_TTL_MS: number;
|
|
|
48
70
|
* NUL rather than a printable separator: a project directory can be called
|
|
49
71
|
* almost anything, and a name holding the separator must not be able to forge
|
|
50
72
|
* another run's key.
|
|
73
|
+
*
|
|
74
|
+
* The run id splits two sittings of the same agent in the same slot: closing
|
|
75
|
+
* `hwip cc` and starting it again is a new conversation, and the record of the
|
|
76
|
+
* first is a past session of its own. An entry without one (written before
|
|
77
|
+
* runs were recorded) keys as the empty run.
|
|
51
78
|
*/
|
|
52
|
-
export declare const runKey: (name: string, agentKind: string) => string;
|
|
79
|
+
export declare const runKey: (name: string, agentKind: string, runId?: string) => string;
|
|
53
80
|
/** Sessions this machine has seen, newest first, expired ones dropped. */
|
|
54
81
|
export declare const knownSessions: (now?: number) => KnownSession[];
|
|
55
82
|
/** One remembered session, or null when this machine never saw that name.
|
|
@@ -65,6 +92,20 @@ export declare const recallSession: (name: string, now?: number) => KnownSession
|
|
|
65
92
|
* decides whether to fall back to the newest run or refuse.
|
|
66
93
|
*/
|
|
67
94
|
export declare const recallRun: (name: string, agentKind: string, now?: number) => KnownSession | null;
|
|
95
|
+
/** tmux sessions remembered per run: an `exit` and `-c` every few minutes all day stays a short list. */
|
|
96
|
+
export declare const MAX_TMUX_RUNS_PER_RUN = 10;
|
|
97
|
+
/**
|
|
98
|
+
* Which run the tmux session `tmuxRunId` of `name` is (hwip docs/session-runs.md §3).
|
|
99
|
+
*
|
|
100
|
+
* Its own, unless it carries on an earlier run: it was already joined to one
|
|
101
|
+
* (a sweep or a listener restart since), or the agent's conversation
|
|
102
|
+
* `conversationId` is the one an earlier run of the same name and agent last
|
|
103
|
+
* had. A tmux session that has had a conversation of its own stays its own run
|
|
104
|
+
* — a fresh start `/resume`d into an old conversation keeps what it wrote first.
|
|
105
|
+
*/
|
|
106
|
+
export declare const continuedRunId: (entries: readonly KnownSession[], name: string, agentKind: string, tmuxRunId: string, conversationId: string | null | undefined) => string;
|
|
107
|
+
/** The run going on in tmux session `tmuxRunId` of `name`: the one it was joined to, else its own. */
|
|
108
|
+
export declare const runOfTmuxSession: (name: string, tmuxRunId: string, now?: number) => string;
|
|
68
109
|
/**
|
|
69
110
|
* Make this process a reader of the registry: `rememberSessions` writes nothing from here on. For the
|
|
70
111
|
* CLI commands that look at the sweep (`hwip agent list|read`) — the file is the listener's resume
|
|
@@ -87,6 +128,16 @@ export declare const rememberSessions: (live: Array<{
|
|
|
87
128
|
agentKind: string;
|
|
88
129
|
project?: string | null;
|
|
89
130
|
label?: string | null;
|
|
131
|
+
runId?: string;
|
|
132
|
+
startedAt?: string;
|
|
133
|
+
/** The tmux session it runs in; one other than `runId` means the run goes on in it (`continuedRunId`). */
|
|
134
|
+
tmuxRunId?: string;
|
|
135
|
+
/**
|
|
136
|
+
* The conversation a resume may pick up. Absent: not readable this
|
|
137
|
+
* sweep, so the run keeps the one it had. Null: read, but not this
|
|
138
|
+
* run's alone to resume — the run forgets it.
|
|
139
|
+
*/
|
|
140
|
+
agentSessionId?: string | null;
|
|
90
141
|
}>, now?: number) => void;
|
|
91
142
|
/**
|
|
92
143
|
* Forget one session, by name. Returns false when this machine never knew it.
|
|
@@ -107,7 +158,8 @@ export type ForgetOutcome =
|
|
|
107
158
|
| 'forgotten'
|
|
108
159
|
/** Registry entry gone, but the scrollback file would not delete. */
|
|
109
160
|
| 'scrollback_kept';
|
|
110
|
-
|
|
161
|
+
/** `nameRunning`: a run of this name is going right now, so its scrollback stays whether or not the registry has written that run down yet. */
|
|
162
|
+
export declare const forgetSession: (name: string, agentKind?: string, runId?: string, nameRunning?: boolean) => ForgetOutcome;
|
|
111
163
|
/**
|
|
112
164
|
* What the caller was able to find out about a name. `unknown` is not a
|
|
113
165
|
* synonym for ended: it is the terminal failing to answer at all — no tmux to
|
package/dist/session-registry.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* another program.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.forgetEnded = exports.forgetSession = exports.rememberSessions = exports.keepRegistryUntouched = exports.recallRun = exports.recallSession = exports.knownSessions = exports.runKey = exports.KNOWN_SESSION_TTL_MS = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
19
|
+
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.forgetEnded = exports.forgetSession = exports.rememberSessions = exports.keepRegistryUntouched = exports.runOfTmuxSession = exports.continuedRunId = exports.MAX_TMUX_RUNS_PER_RUN = exports.recallRun = exports.recallSession = exports.knownSessions = exports.runKey = exports.KNOWN_SESSION_TTL_MS = exports.MAX_RUNS_PER_NAME = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
20
20
|
const fs_1 = require("fs");
|
|
21
21
|
const path_1 = require("path");
|
|
22
22
|
const turn_ring_js_1 = require("./turn-ring.js");
|
|
@@ -29,10 +29,13 @@ const private_files_js_1 = require("./private-files.js");
|
|
|
29
29
|
* that has run agents for years.
|
|
30
30
|
*/
|
|
31
31
|
exports.MAX_KNOWN_SESSIONS = 100;
|
|
32
|
+
/** Runs kept per name: a slot reused all day would otherwise fill the file with one project. */
|
|
33
|
+
exports.MAX_RUNS_PER_NAME = 10;
|
|
32
34
|
/** Forgotten after this long unseen — a directory from months ago is more
|
|
33
35
|
* likely to have moved than to be what the user meant. */
|
|
34
36
|
exports.KNOWN_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
35
37
|
const registryPath = () => (0, path_1.join)((0, gate_js_1.stateDir)(), 'known-sessions.json');
|
|
38
|
+
const isTmuxRunIds = (v) => v === undefined || (Array.isArray(v) && v.every((id) => typeof id === 'string'));
|
|
36
39
|
const readAll = () => {
|
|
37
40
|
try {
|
|
38
41
|
const raw = (0, fs_1.readFileSync)(registryPath(), 'utf-8');
|
|
@@ -46,7 +49,9 @@ const readAll = () => {
|
|
|
46
49
|
typeof e.name === 'string' &&
|
|
47
50
|
typeof e.cwd === 'string' &&
|
|
48
51
|
typeof e.agentKind === 'string' &&
|
|
49
|
-
typeof e.lastSeenAt === 'string'
|
|
52
|
+
typeof e.lastSeenAt === 'string' &&
|
|
53
|
+
(e.runId === undefined || typeof e.runId === 'string') &&
|
|
54
|
+
isTmuxRunIds(e.tmuxRunIds));
|
|
50
55
|
}
|
|
51
56
|
catch {
|
|
52
57
|
return [];
|
|
@@ -67,8 +72,8 @@ const writeAll = (entries) => {
|
|
|
67
72
|
}
|
|
68
73
|
};
|
|
69
74
|
/**
|
|
70
|
-
* What makes two sightings the same remembered run: the tmux name
|
|
71
|
-
*
|
|
75
|
+
* What makes two sightings the same remembered run: the tmux name, the agent
|
|
76
|
+
* that was running under it, AND the tmux session it ran in (`runId`).
|
|
72
77
|
*
|
|
73
78
|
* Keyed by name alone, a `hwip cc` in a project erased whatever pi had done in
|
|
74
79
|
* the same slot — the ended run left no row to resume and no row to read, and
|
|
@@ -78,8 +83,13 @@ const writeAll = (entries) => {
|
|
|
78
83
|
* NUL rather than a printable separator: a project directory can be called
|
|
79
84
|
* almost anything, and a name holding the separator must not be able to forge
|
|
80
85
|
* another run's key.
|
|
86
|
+
*
|
|
87
|
+
* The run id splits two sittings of the same agent in the same slot: closing
|
|
88
|
+
* `hwip cc` and starting it again is a new conversation, and the record of the
|
|
89
|
+
* first is a past session of its own. An entry without one (written before
|
|
90
|
+
* runs were recorded) keys as the empty run.
|
|
81
91
|
*/
|
|
82
|
-
const runKey = (name, agentKind) => `${name}\u0000${agentKind}`;
|
|
92
|
+
const runKey = (name, agentKind, runId) => `${name}\u0000${agentKind}\u0000${runId ?? ''}`;
|
|
83
93
|
exports.runKey = runKey;
|
|
84
94
|
/** Sessions this machine has seen, newest first, expired ones dropped. */
|
|
85
95
|
const knownSessions = (now = Date.now()) => readAll()
|
|
@@ -101,6 +111,37 @@ exports.recallSession = recallSession;
|
|
|
101
111
|
*/
|
|
102
112
|
const recallRun = (name, agentKind, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name && e.agentKind === agentKind) ?? null;
|
|
103
113
|
exports.recallRun = recallRun;
|
|
114
|
+
/** tmux sessions remembered per run: an `exit` and `-c` every few minutes all day stays a short list. */
|
|
115
|
+
exports.MAX_TMUX_RUNS_PER_RUN = 10;
|
|
116
|
+
/** Agents whose own resume is measured to keep the conversation id: Claude Code 2.1.300's `-c` does. */
|
|
117
|
+
const CONTINUING_KINDS = new Set(['claude']);
|
|
118
|
+
/**
|
|
119
|
+
* Which run the tmux session `tmuxRunId` of `name` is (hwip docs/session-runs.md §3).
|
|
120
|
+
*
|
|
121
|
+
* Its own, unless it carries on an earlier run: it was already joined to one
|
|
122
|
+
* (a sweep or a listener restart since), or the agent's conversation
|
|
123
|
+
* `conversationId` is the one an earlier run of the same name and agent last
|
|
124
|
+
* had. A tmux session that has had a conversation of its own stays its own run
|
|
125
|
+
* — a fresh start `/resume`d into an old conversation keeps what it wrote first.
|
|
126
|
+
*/
|
|
127
|
+
const continuedRunId = (entries, name, agentKind, tmuxRunId, conversationId) => {
|
|
128
|
+
const ofName = entries.filter((e) => e.name === name && e.agentKind === agentKind && e.runId);
|
|
129
|
+
const joined = ofName.find((e) => e.runId !== tmuxRunId && e.tmuxRunIds?.includes(tmuxRunId));
|
|
130
|
+
if (joined?.runId)
|
|
131
|
+
return joined.runId;
|
|
132
|
+
if (!conversationId || !CONTINUING_KINDS.has(agentKind))
|
|
133
|
+
return tmuxRunId;
|
|
134
|
+
if (ofName.some((e) => e.runId === tmuxRunId && e.agentSessionId))
|
|
135
|
+
return tmuxRunId;
|
|
136
|
+
const earlier = ofName
|
|
137
|
+
.filter((e) => e.agentSessionId === conversationId && Number(e.runId) < Number(tmuxRunId))
|
|
138
|
+
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))[0];
|
|
139
|
+
return earlier?.runId ?? tmuxRunId;
|
|
140
|
+
};
|
|
141
|
+
exports.continuedRunId = continuedRunId;
|
|
142
|
+
/** The run going on in tmux session `tmuxRunId` of `name`: the one it was joined to, else its own. */
|
|
143
|
+
const runOfTmuxSession = (name, tmuxRunId, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name && e.tmuxRunIds?.includes(tmuxRunId))?.runId ?? tmuxRunId;
|
|
144
|
+
exports.runOfTmuxSession = runOfTmuxSession;
|
|
104
145
|
let readOnly = false;
|
|
105
146
|
/**
|
|
106
147
|
* Make this process a reader of the registry: `rememberSessions` writes nothing from here on. For the
|
|
@@ -128,30 +169,68 @@ const rememberSessions = (live, now = Date.now()) => {
|
|
|
128
169
|
if (usable.length === 0)
|
|
129
170
|
return;
|
|
130
171
|
const seenAt = new Date(now).toISOString();
|
|
131
|
-
const byRun = new Map((0, exports.knownSessions)(now).map((e) => [(0, exports.runKey)(e.name, e.agentKind), e]));
|
|
172
|
+
const byRun = new Map((0, exports.knownSessions)(now).map((e) => [(0, exports.runKey)(e.name, e.agentKind, e.runId), e]));
|
|
132
173
|
for (const s of usable) {
|
|
133
|
-
|
|
174
|
+
// The entry this run had before runs were recorded: same slot and agent,
|
|
175
|
+
// no run, seen while this run was already going. It is this run, not a
|
|
176
|
+
// past one, and would otherwise be reported as one.
|
|
177
|
+
if (s.runId && s.startedAt) {
|
|
178
|
+
const before = byRun.get((0, exports.runKey)(s.name, s.agentKind));
|
|
179
|
+
if (before && before.lastSeenAt >= s.startedAt)
|
|
180
|
+
byRun.delete((0, exports.runKey)(s.name, s.agentKind));
|
|
181
|
+
}
|
|
182
|
+
// Carried on in a new tmux session: what an earlier sweep wrote under that session is this run.
|
|
183
|
+
const continued = !!s.runId && !!s.tmuxRunId && s.tmuxRunId !== s.runId;
|
|
184
|
+
if (continued)
|
|
185
|
+
byRun.delete((0, exports.runKey)(s.name, s.agentKind, s.tmuxRunId));
|
|
186
|
+
const key = (0, exports.runKey)(s.name, s.agentKind, s.runId);
|
|
187
|
+
// A sweep that could not read the id — the agent has not written its
|
|
188
|
+
// record yet — keeps the one this run already had. Keyed by run and
|
|
189
|
+
// directory, so neither another agent taking the name nor the same
|
|
190
|
+
// agent started elsewhere under it inherits a conversation.
|
|
191
|
+
const prev = byRun.get(key);
|
|
192
|
+
const keepPrev = s.agentSessionId === undefined && prev?.cwd === s.cwd;
|
|
193
|
+
const agentSessionId = s.agentSessionId || (keepPrev ? prev?.agentSessionId : undefined);
|
|
194
|
+
// A run keeps its first start whichever tmux session it goes on in.
|
|
195
|
+
const startedAt = continued ? prev?.startedAt ?? s.startedAt : s.startedAt;
|
|
196
|
+
const tmuxRunIds = continued && s.tmuxRunId
|
|
197
|
+
? [...(prev?.tmuxRunIds ?? []).filter((id) => id !== s.tmuxRunId), s.tmuxRunId].slice(-exports.MAX_TMUX_RUNS_PER_RUN)
|
|
198
|
+
: prev?.tmuxRunIds;
|
|
199
|
+
byRun.set(key, {
|
|
134
200
|
name: s.name,
|
|
135
201
|
cwd: s.cwd,
|
|
136
202
|
agentKind: s.agentKind,
|
|
137
203
|
...(s.project ? { project: s.project } : {}),
|
|
138
204
|
...(s.label ? { label: s.label } : {}),
|
|
205
|
+
...(s.runId ? { runId: s.runId } : {}),
|
|
206
|
+
...(startedAt ? { startedAt } : {}),
|
|
207
|
+
...(agentSessionId ? { agentSessionId } : {}),
|
|
208
|
+
...(tmuxRunIds?.length ? { tmuxRunIds } : {}),
|
|
139
209
|
lastSeenAt: seenAt,
|
|
140
210
|
});
|
|
141
211
|
}
|
|
212
|
+
const perName = new Map();
|
|
142
213
|
const entries = [...byRun.values()]
|
|
143
214
|
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
|
|
215
|
+
.filter((e) => {
|
|
216
|
+
const n = (perName.get(e.name) ?? 0) + 1;
|
|
217
|
+
perName.set(e.name, n);
|
|
218
|
+
return n <= exports.MAX_RUNS_PER_NAME;
|
|
219
|
+
})
|
|
144
220
|
.slice(0, exports.MAX_KNOWN_SESSIONS);
|
|
145
221
|
writeAll(entries);
|
|
146
222
|
};
|
|
147
223
|
exports.rememberSessions = rememberSessions;
|
|
148
|
-
|
|
224
|
+
/** `nameRunning`: a run of this name is going right now, so its scrollback stays whether or not the registry has written that run down yet. */
|
|
225
|
+
const forgetSession = (name, agentKind, runId, nameRunning = false) => {
|
|
149
226
|
const entries = readAll();
|
|
150
|
-
// With a kind, only that
|
|
151
|
-
// at, and a name that held both pi and claude shows two rows.
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
227
|
+
// With a kind, only that agent's runs go — the phone deletes the row it is
|
|
228
|
+
// looking at, and a name that held both pi and claude shows two rows. With a
|
|
229
|
+
// run as well, only that one run. Without a kind (an older phone, which
|
|
230
|
+
// sends none), the name goes entirely: that is what the request meant when a
|
|
231
|
+
// name could only mean one run.
|
|
232
|
+
const doomed = (e) => e.name === name && (!agentKind || e.agentKind === agentKind) && (!runId || e.runId === runId);
|
|
233
|
+
const kept = entries.filter((e) => !doomed(e));
|
|
155
234
|
if (kept.length === entries.length)
|
|
156
235
|
return 'unknown';
|
|
157
236
|
// The chat's scrollback for that session goes with it. Forgetting a session
|
|
@@ -162,7 +241,7 @@ const forgetSession = (name, agentKind) => {
|
|
|
162
241
|
// Except while another run still answers to the name: the scrollback is per
|
|
163
242
|
// NAME, so deleting the pi row would take the claude row's history with it.
|
|
164
243
|
// The record is gone either way; the file waits for the last run to go.
|
|
165
|
-
const nameStillHeld = kept.some((e) => e.name === name);
|
|
244
|
+
const nameStillHeld = nameRunning || kept.some((e) => e.name === name);
|
|
166
245
|
writeAll(kept);
|
|
167
246
|
if (nameStillHeld)
|
|
168
247
|
return 'forgotten';
|
package/dist/templates.js
CHANGED
|
@@ -368,6 +368,7 @@ exports.COPILOT_HOOKS = JSON.stringify({
|
|
|
368
368
|
/** Pi extension source — written to ~/.pi/agent/extensions/hwip.ts (drop-in auto-load). */
|
|
369
369
|
exports.PI_EXTENSION = `// Generated by @hwip/cli — reinstall overwrites; \`hwip uninstall\` removes.
|
|
370
370
|
import { spawn, execFile, execFileSync } from "node:child_process";
|
|
371
|
+
import { statSync } from "node:fs";
|
|
371
372
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
372
373
|
|
|
373
374
|
const sh = (cmd: string, cwd: string, stdin?: string): Promise<string> =>
|
|
@@ -436,14 +437,21 @@ const SUBAGENT_SESSION = (() => {
|
|
|
436
437
|
// to the pane the wrapper records as session options (cli config
|
|
437
438
|
// HERDR_SESSION_OPTIONS). Read per report, not once: a reattach from another
|
|
438
439
|
// herdr pane retargets a running pi. No options -> not in herdr -> no-op. herdr's
|
|
439
|
-
// own pi extension reads HERDR_* from the env, which
|
|
440
|
-
//
|
|
440
|
+
// own pi extension reads HERDR_* from the env, which the wrapper empties for a
|
|
441
|
+
// \`hwip pi\` session (cli wrapper herdrEnvArgs), so it stays silent here: a second
|
|
442
|
+
// report on the pane would replace this one. Same protocol as the Claude Code
|
|
443
|
+
// plugin's hwip-herdr.sh.
|
|
441
444
|
const TMUX_PANE = process.env.TMUX_PANE || "";
|
|
442
445
|
// Set by session_start for the TUI only: a \`pi -p\` or RPC run from a shell in
|
|
443
446
|
// a wrapped pane inherits TMUX_PANE, and must not report (or release) that
|
|
444
447
|
// pane's agent. herdr's own pi extension gates on the same \`ctx.mode\`.
|
|
445
448
|
let herdrTui = false;
|
|
446
449
|
let herdrLast = "";
|
|
450
|
+
// herdr forgets every report when it restarts, and recreates its socket when it
|
|
451
|
+
// starts: the socket's mtime is part of what counts as already sent.
|
|
452
|
+
const herdrBorn = (socket: string): number => {
|
|
453
|
+
try { return statSync(socket).mtimeMs; } catch { return 0; }
|
|
454
|
+
};
|
|
447
455
|
let herdrSeq = 0;
|
|
448
456
|
// Resolves once herdr is spawned, so session_shutdown can hold pi's exit until
|
|
449
457
|
// the release is on its way.
|
|
@@ -454,9 +462,12 @@ const herdr = (state: "idle" | "working" | "blocked" | "release"): Promise<void>
|
|
|
454
462
|
// it now, in event order, not when tmux answers.
|
|
455
463
|
const seq = herdrSeq = Math.max(Date.now() * 1000, herdrSeq + 1);
|
|
456
464
|
execFile("tmux", ["display-message", "-p", "-t", TMUX_PANE, "#{@hwip_herdr_pane}\\t#{@hwip_herdr_socket}\\t#{@hwip_herdr_bin}"], { timeout: 2000 }, (err, out) => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
465
|
+
// Only the newline off the end: trim() would also eat an empty pane's tab and
|
|
466
|
+
// read the socket as the pane.
|
|
467
|
+
const [pane, socket, bin] = String(out).replace(/\\r?\\n$/, "").split("\\t");
|
|
468
|
+
const sent = pane + " " + state + " " + (socket ? herdrBorn(socket) : 0);
|
|
469
|
+
if (err || !pane || !socket || herdrLast === sent) return resolve();
|
|
470
|
+
herdrLast = sent;
|
|
460
471
|
const who = ["--source", "hwip-pi", "--agent", "hwip pi"];
|
|
461
472
|
const args = state === "release"
|
|
462
473
|
? ["pane", "release-agent", pane, ...who, "--seq", String(seq)]
|
package/dist/tmux-liveness.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ import type { SessionLiveness } from './session-registry.js';
|
|
|
11
11
|
* user's server would otherwise call every live session an ended one.
|
|
12
12
|
*/
|
|
13
13
|
export declare const liveSessionNames: () => string[] | null;
|
|
14
|
+
/**
|
|
15
|
+
* The run (hwip docs/session-runs.md) tmux holds under `name` right now: its
|
|
16
|
+
* `session_created` in epoch seconds. Null when the name is not running, and
|
|
17
|
+
* undefined when no tmux answered — a caller that deletes on "another run"
|
|
18
|
+
* must refuse on that, as it refuses on `unknown` liveness.
|
|
19
|
+
*/
|
|
20
|
+
export declare const liveRunIdOf: (name: string) => string | null | undefined;
|
|
14
21
|
/**
|
|
15
22
|
* The three-state answer, for callers that destroy something on "ended".
|
|
16
23
|
*
|
package/dist/tmux-liveness.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sessionExists = exports.sessionLiveness = exports.liveSessionNames = void 0;
|
|
3
|
+
exports.sessionExists = exports.sessionLiveness = exports.liveRunIdOf = exports.liveSessionNames = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const tmux_pane_js_1 = require("./tmux-pane.js");
|
|
6
6
|
const tmux_socket_js_1 = require("./tmux-socket.js");
|
|
@@ -27,6 +27,27 @@ const liveSessionNames = () => {
|
|
|
27
27
|
return (probe.stdout ?? '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
28
28
|
};
|
|
29
29
|
exports.liveSessionNames = liveSessionNames;
|
|
30
|
+
/**
|
|
31
|
+
* The run (hwip docs/session-runs.md) tmux holds under `name` right now: its
|
|
32
|
+
* `session_created` in epoch seconds. Null when the name is not running, and
|
|
33
|
+
* undefined when no tmux answered — a caller that deletes on "another run"
|
|
34
|
+
* must refuse on that, as it refuses on `unknown` liveness.
|
|
35
|
+
*/
|
|
36
|
+
const liveRunIdOf = (name) => {
|
|
37
|
+
const probe = (0, child_process_1.spawnSync)('tmux', (0, tmux_socket_js_1.tmuxArgs)(['list-sessions', '-F', '#{session_name}\t#{session_created}']), {
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
40
|
+
});
|
|
41
|
+
if (probe.error || probe.status !== 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
for (const line of (probe.stdout ?? '').split('\n')) {
|
|
44
|
+
const tab = line.lastIndexOf('\t');
|
|
45
|
+
if (tab > 0 && line.slice(0, tab) === name)
|
|
46
|
+
return /^\d{1,12}$/.test(line.slice(tab + 1).trim()) ? line.slice(tab + 1).trim() : undefined;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
exports.liveRunIdOf = liveRunIdOf;
|
|
30
51
|
/**
|
|
31
52
|
* The three-state answer, for callers that destroy something on "ended".
|
|
32
53
|
*
|
|
@@ -98,7 +98,7 @@ const createChunkUploader = (opts) => {
|
|
|
98
98
|
try {
|
|
99
99
|
res = await (0, api_js_1.apiFetch)(fetchFn, (0, api_js_1.apiUrl)(opts.baseUrl, `/sessions/${encodeURIComponent(sessionKey)}/turns`), opts.apiKey, UPLOAD_TIMEOUT_MS, {
|
|
100
100
|
method: 'POST',
|
|
101
|
-
body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt, sealed: chunk.sealed }),
|
|
101
|
+
body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt, ...(chunk.runId ? { runId: chunk.runId } : {}), sealed: chunk.sealed }),
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
catch (err) {
|
package/dist/turn-recorder.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface RecordableSession {
|
|
|
80
80
|
path: string;
|
|
81
81
|
/** `RegisteredRemoteAgent.kind` — which projector reads it. */
|
|
82
82
|
kind: string;
|
|
83
|
+
/** The run (tmux session) writing it (hwip docs/session-runs.md); every chunk read from it carries this. */
|
|
84
|
+
runId?: string;
|
|
83
85
|
}
|
|
84
86
|
/** A pinned device's key, or this machine's own. */
|
|
85
87
|
export interface RecorderRecipient {
|
|
@@ -96,6 +98,8 @@ export interface OutboxChunk {
|
|
|
96
98
|
seq: number;
|
|
97
99
|
firstAt: string;
|
|
98
100
|
lastAt: string;
|
|
101
|
+
/** The run the events were recorded in. Absent from chunks made before runs were recorded. */
|
|
102
|
+
runId?: string;
|
|
99
103
|
sealed: SealedChunk;
|
|
100
104
|
}
|
|
101
105
|
export type UploadOutcome =
|
|
@@ -145,6 +149,8 @@ export interface TurnRecorderDeps {
|
|
|
145
149
|
/** The newest `seq` the server holds for a session, or null when it cannot be read. After a 409
|
|
146
150
|
* (the same `seq` under another body) the next chunk is numbered past it. */
|
|
147
151
|
latestSeq?: (sessionKey: string) => Promise<number | null>;
|
|
152
|
+
/** The seqs a run was just handed, once they are in the outbox: the run's seq block is reported from these (`run-seqs.ts`). */
|
|
153
|
+
noteSeqs?: (name: string, runId: string, firstSeq: number, lastSeq: number) => void;
|
|
148
154
|
log: (message: string) => void;
|
|
149
155
|
now?: () => number;
|
|
150
156
|
}
|
|
@@ -155,6 +161,8 @@ interface SessionState {
|
|
|
155
161
|
path: string;
|
|
156
162
|
/** The agent kind that wrote it, so a session that is gone after a restart can still be read to its end. */
|
|
157
163
|
kind?: string;
|
|
164
|
+
/** The run that wrote it, for the same reason: what is read from it after a restart is still that run's. */
|
|
165
|
+
runId?: string;
|
|
158
166
|
ino: number;
|
|
159
167
|
/** End of the last complete line whose events are in a chunk. */
|
|
160
168
|
offset: number;
|
package/dist/turn-recorder.js
CHANGED
|
@@ -107,6 +107,7 @@ const readSessionState = (dir, sessionKey) => {
|
|
|
107
107
|
sessionKey,
|
|
108
108
|
path: raw.path,
|
|
109
109
|
...(typeof raw.kind === 'string' ? { kind: raw.kind } : {}),
|
|
110
|
+
...(typeof raw.runId === 'string' ? { runId: raw.runId } : {}),
|
|
110
111
|
ino: typeof raw.ino === 'number' ? raw.ino : 0,
|
|
111
112
|
offset: raw.offset,
|
|
112
113
|
...(raw.cursor !== undefined ? { cursor: raw.cursor } : {}),
|
|
@@ -212,6 +213,9 @@ const createTurnRecorder = (deps) => {
|
|
|
212
213
|
/** The kind that wrote `path`: the file a session has left was written by the kind its state names,
|
|
213
214
|
* which is not always the kind of the file it writes now. */
|
|
214
215
|
const kindOf = (live, path) => path !== live.source.path ? live.state?.kind ?? live.source.kind : live.source.kind;
|
|
216
|
+
/** The run that wrote `path`, by the same rule: a file the session has left is the run its state names.
|
|
217
|
+
* A new run under the same name keeps the name's `seq` numbering (the server's key) but tags its own. */
|
|
218
|
+
const runOf = (live, path) => path !== live.source.path ? live.state?.runId ?? live.source.runId : live.source.runId;
|
|
215
219
|
const readerOf = (live, path) => deps.readerFor?.(kindOf(live, path)) ?? null;
|
|
216
220
|
const startTail = (live) => {
|
|
217
221
|
const state = live.state;
|
|
@@ -267,6 +271,7 @@ const createTurnRecorder = (deps) => {
|
|
|
267
271
|
};
|
|
268
272
|
const fallbackAt = new Date(now()).toISOString();
|
|
269
273
|
const chunks = [];
|
|
274
|
+
const runId = runOf(live, live.tailPath);
|
|
270
275
|
let seq = base.seq;
|
|
271
276
|
try {
|
|
272
277
|
for (const page of pages) {
|
|
@@ -274,7 +279,7 @@ const createTurnRecorder = (deps) => {
|
|
|
274
279
|
const firstAt = eventAt(page, 'first', fallbackAt);
|
|
275
280
|
const lastAt = eventAt(page, 'last', fallbackAt);
|
|
276
281
|
const sealed = await deps.seal(JSON.stringify({ sessionKey: live.sessionKey, seq, events: page }), recipients);
|
|
277
|
-
chunks.push({ seq, firstAt, lastAt, sealed });
|
|
282
|
+
chunks.push({ seq, firstAt, lastAt, ...(runId ? { runId } : {}), sealed });
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
catch (err) {
|
|
@@ -291,6 +296,7 @@ const createTurnRecorder = (deps) => {
|
|
|
291
296
|
...base,
|
|
292
297
|
path: live.tailPath,
|
|
293
298
|
kind: kindOf(live, live.tailPath),
|
|
299
|
+
runId,
|
|
294
300
|
ino: live.tail.ino,
|
|
295
301
|
offset: committed,
|
|
296
302
|
cursor: live.tail.cursor,
|
|
@@ -300,6 +306,8 @@ const createTurnRecorder = (deps) => {
|
|
|
300
306
|
};
|
|
301
307
|
if (!persist(live, next))
|
|
302
308
|
return;
|
|
309
|
+
if (runId && chunks.length)
|
|
310
|
+
deps.noteSeqs?.(live.name, runId, chunks[0].seq, seq);
|
|
303
311
|
live.pending = [];
|
|
304
312
|
live.pendingBytes = 0;
|
|
305
313
|
live.hasPrompt = false;
|
|
@@ -615,7 +623,7 @@ const createTurnRecorder = (deps) => {
|
|
|
615
623
|
const state = (0, exports.readSessionState)(deps.dir, sessionKey);
|
|
616
624
|
if (!state)
|
|
617
625
|
continue;
|
|
618
|
-
sessions.set(name, newLive(name, { path: state.path, kind: state.kind ?? '' }, now()));
|
|
626
|
+
sessions.set(name, newLive(name, { path: state.path, kind: state.kind ?? '', ...(state.runId ? { runId: state.runId } : {}) }, now()));
|
|
619
627
|
}
|
|
620
628
|
};
|
|
621
629
|
/**
|
package/dist/wrapper.d.ts
CHANGED
|
@@ -20,12 +20,20 @@ export declare const detectProjectName: () => string;
|
|
|
20
20
|
* session (measured on tmux 3.5a).
|
|
21
21
|
*/
|
|
22
22
|
export declare const sessionSidecarArgs: (session: string, projectDir: string, label?: string) => string[];
|
|
23
|
+
export declare const herdrSessionArgs: (session: string, env?: NodeJS.ProcessEnv) => string[];
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* `new-session -e` flags for config `HERDR_ENV_VARS`, so herdr's native integration
|
|
26
|
+
* in the agent reports to the herdr pane this launch attaches from — or, empty, to no
|
|
27
|
+
* pane at all: for an agent hwip reports itself (`HERDR_SELF_REPORTING_KINDS`), a
|
|
28
|
+
* detached launch, or a launch from outside herdr. Without them the agent inherits the
|
|
29
|
+
* tmux server's global env and reports to a pane that is gone. Session env, so a later
|
|
30
|
+
* split pane gets the same. A reattach does not move a running agent's env: herdr's
|
|
31
|
+
* integrations keep the pane the session was created from. `new-session -e` is tmux 3.2;
|
|
32
|
+
* an older tmux gets none, as before.
|
|
27
33
|
*/
|
|
28
|
-
export declare const
|
|
34
|
+
export declare const herdrEnvArgs: (agentKind: string | undefined, env: NodeJS.ProcessEnv, tmuxVersion: string) => string[];
|
|
35
|
+
/** `tmux -V`, read once per process; empty when tmux does not answer. */
|
|
36
|
+
export declare const installedTmuxVersion: () => string;
|
|
29
37
|
/**
|
|
30
38
|
* `hwip-<project>` — the canonical tmux session base name. tmux itself turns
|
|
31
39
|
* `.` and `:` into `_` when it creates a session (3.5a: `hwip-foo.bar` becomes
|
package/dist/wrapper.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleShellSession = exports.shellTargets = exports.readShellSessionState = exports.planShellSession = exports.handleAgentSession = exports.tmuxMissingMessage = exports.inPaneNote = exports.handOffExec = exports.ensureListenerRunning = exports.takeListenerLaunchMarker = exports.listenerVersionDrifted = exports.targetForAgent = exports.splitAgentOptions = exports.sanitizeLabel = exports.findAvailableSession = exports.firstFreeSession = exports.tmuxSessionName = exports.herdrSessionArgs = exports.sessionSidecarArgs = exports.detectProjectName = exports.detectProjectDir = exports.safeBasename = void 0;
|
|
3
|
+
exports.handleShellSession = exports.shellTargets = exports.readShellSessionState = exports.planShellSession = exports.handleAgentSession = exports.tmuxMissingMessage = exports.inPaneNote = exports.handOffExec = exports.ensureListenerRunning = exports.takeListenerLaunchMarker = exports.listenerVersionDrifted = exports.targetForAgent = exports.splitAgentOptions = exports.sanitizeLabel = exports.findAvailableSession = exports.firstFreeSession = exports.tmuxSessionName = exports.installedTmuxVersion = exports.herdrEnvArgs = exports.herdrSessionArgs = exports.sessionSidecarArgs = exports.detectProjectName = exports.detectProjectDir = exports.safeBasename = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `hwip cc` / `hwip codex` / `hwip gemini` — spawn an agent inside a named
|
|
6
6
|
* tmux session so the resident listener (`hwip listener`) can address it
|
|
@@ -79,14 +79,52 @@ exports.sessionSidecarArgs = sessionSidecarArgs;
|
|
|
79
79
|
* `HERDR_SESSION_OPTIONS` at the herdr pane this attach runs in, or clear them
|
|
80
80
|
* outside herdr.
|
|
81
81
|
*/
|
|
82
|
+
const inHerdr = (env) => env.HERDR_ENV === '1' && !!env.HERDR_PANE_ID && !!env.HERDR_SOCKET_PATH;
|
|
82
83
|
const herdrSessionArgs = (session, env = process.env) => {
|
|
83
|
-
const
|
|
84
|
+
const attached = inHerdr(env);
|
|
84
85
|
return Object.entries(config_js_1.HERDR_SESSION_OPTIONS).flatMap(([name, option]) => {
|
|
85
|
-
const value =
|
|
86
|
+
const value = attached ? env[name] : undefined;
|
|
86
87
|
return [';', 'set-option', ...(value ? [] : ['-u']), '-t', sessionOptionTarget(session), option, ...(value ? [value] : [])];
|
|
87
88
|
});
|
|
88
89
|
};
|
|
89
90
|
exports.herdrSessionArgs = herdrSessionArgs;
|
|
91
|
+
/**
|
|
92
|
+
* `new-session -e` flags for config `HERDR_ENV_VARS`, so herdr's native integration
|
|
93
|
+
* in the agent reports to the herdr pane this launch attaches from — or, empty, to no
|
|
94
|
+
* pane at all: for an agent hwip reports itself (`HERDR_SELF_REPORTING_KINDS`), a
|
|
95
|
+
* detached launch, or a launch from outside herdr. Without them the agent inherits the
|
|
96
|
+
* tmux server's global env and reports to a pane that is gone. Session env, so a later
|
|
97
|
+
* split pane gets the same. A reattach does not move a running agent's env: herdr's
|
|
98
|
+
* integrations keep the pane the session was created from. `new-session -e` is tmux 3.2;
|
|
99
|
+
* an older tmux gets none, as before.
|
|
100
|
+
*/
|
|
101
|
+
const herdrEnvArgs = (agentKind, env, tmuxVersion) => {
|
|
102
|
+
if (!sessionEnvSupported(tmuxVersion))
|
|
103
|
+
return [];
|
|
104
|
+
const native = inHerdr(env) && !config_js_1.HERDR_SELF_REPORTING_KINDS.has(agentKind ?? '');
|
|
105
|
+
return config_js_1.HERDR_ENV_VARS.flatMap((name) => ['-e', `${name}=${native ? env[name] ?? '' : ''}`]);
|
|
106
|
+
};
|
|
107
|
+
exports.herdrEnvArgs = herdrEnvArgs;
|
|
108
|
+
/** `tmux -V` says 3.2 or later (`new-session -e`); a development build (`next-3.7`, `master`) is newer. */
|
|
109
|
+
const sessionEnvSupported = (version) => {
|
|
110
|
+
if (/\b(master|next-)/.test(version))
|
|
111
|
+
return true;
|
|
112
|
+
const m = version.match(/(\d+)\.(\d+)/);
|
|
113
|
+
if (!m)
|
|
114
|
+
return false;
|
|
115
|
+
const [major, minor] = [Number(m[1]), Number(m[2])];
|
|
116
|
+
return major > 3 || (major === 3 && minor >= 2);
|
|
117
|
+
};
|
|
118
|
+
let tmuxVersionCache;
|
|
119
|
+
/** `tmux -V`, read once per process; empty when tmux does not answer. */
|
|
120
|
+
const installedTmuxVersion = () => {
|
|
121
|
+
if (tmuxVersionCache === undefined) {
|
|
122
|
+
const ver = (0, child_process_1.spawnSync)('tmux', ['-V'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
123
|
+
tmuxVersionCache = (ver.stdout ?? '').trim();
|
|
124
|
+
}
|
|
125
|
+
return tmuxVersionCache;
|
|
126
|
+
};
|
|
127
|
+
exports.installedTmuxVersion = installedTmuxVersion;
|
|
90
128
|
/**
|
|
91
129
|
* The option target for one session by exact name. `=` stops a prefix match;
|
|
92
130
|
* the trailing `:` makes `show-options`/`set-option`, which read `-t` as a
|
|
@@ -300,10 +338,13 @@ const targetForAgent = (agent, extra, agentKind, opts = {}) => {
|
|
|
300
338
|
// would otherwise get a session named for one tree running in another.
|
|
301
339
|
const sidecar = (0, exports.sessionSidecarArgs)(session, projectDir, opts.label);
|
|
302
340
|
if (opts.detach) {
|
|
303
|
-
|
|
341
|
+
// Attached to no herdr pane: blank, whatever pane this command was typed in.
|
|
342
|
+
const herdrEnv = (0, exports.herdrEnvArgs)(agentKind, {}, (0, exports.installedTmuxVersion)());
|
|
343
|
+
return { kind: 'tmux-detached', cmd: 'tmux', args: ['new', '-d', '-s', session, '-c', projectDir, ...herdrEnv, shellCmd, ...sidecar], session };
|
|
304
344
|
}
|
|
305
|
-
// `tmux new -A`: attach if the named session exists, else create it.
|
|
306
|
-
|
|
345
|
+
// `tmux new -A`: attach if the named session exists, else create it. `-e` applies only when it creates.
|
|
346
|
+
const herdrEnv = (0, exports.herdrEnvArgs)(agentKind, process.env, (0, exports.installedTmuxVersion)());
|
|
347
|
+
return { kind: 'tmux-new', cmd: 'tmux', args: ['new', '-A', '-s', session, ...herdrEnv, shellCmd, ...sidecar, ...(0, exports.herdrSessionArgs)(session)], session };
|
|
307
348
|
};
|
|
308
349
|
exports.targetForAgent = targetForAgent;
|
|
309
350
|
// ── Background listener auto-start ────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hwip/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Hwip CLI + push notification SDK for AI agents",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -43,7 +43,21 @@
|
|
|
43
43
|
"main"
|
|
44
44
|
],
|
|
45
45
|
"plugins": [
|
|
46
|
-
|
|
46
|
+
[
|
|
47
|
+
"@semantic-release/commit-analyzer",
|
|
48
|
+
{
|
|
49
|
+
"releaseRules": [
|
|
50
|
+
{
|
|
51
|
+
"breaking": true,
|
|
52
|
+
"release": "patch"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "feat",
|
|
56
|
+
"release": "patch"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
],
|
|
47
61
|
"@semantic-release/release-notes-generator",
|
|
48
62
|
"@semantic-release/npm",
|
|
49
63
|
"@semantic-release/github"
|