@hwip/cli 0.1.19 → 0.1.21
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 +11 -5
- package/dist/device-presence.d.ts +2 -0
- package/dist/device-presence.js +4 -2
- package/dist/installer.d.ts +3 -0
- package/dist/installer.js +6 -1
- package/dist/listener.d.ts +2 -0
- package/dist/listener.js +37 -3
- package/dist/recent-projects.d.ts +10 -4
- package/dist/recent-projects.js +20 -12
- package/dist/transcript-tail.js +7 -3
- package/dist/wrapper.d.ts +11 -1
- package/dist/wrapper.js +18 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -435,9 +435,12 @@ block here.
|
|
|
435
435
|
real terminal: `cd` and `export` stick, `sudo`/`ssh` prompts work, and a
|
|
436
436
|
long command does not tie up an agent. `--label <x>` names it
|
|
437
437
|
`hwip-<project>-<x>`; `--detach` creates it without attaching. Away from
|
|
438
|
-
the desk, open one from the phone
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
the desk, open one from the phone's **New session → Terminal**: the
|
|
439
|
+
listener runs the same `hwip sh --detach` in the directory the chosen
|
|
440
|
+
project last ran in (`session-shell-v1`; the phone names a session it saw,
|
|
441
|
+
never a directory). Through any running Claude Code session, sending
|
|
442
|
+
`!hwip sh --detach` does the same (Claude Code's `!` bash mode — see
|
|
443
|
+
Defense item 7). Inside tmux it never attaches; it prints a
|
|
441
444
|
`tmux switch-client -t <session>` line instead. Running it again for a
|
|
442
445
|
session it opened reattaches, and a session of that name it did not open
|
|
443
446
|
is refused.
|
|
@@ -980,8 +983,11 @@ into a shell-adjacent pane). The defense is layered:
|
|
|
980
983
|
What the marker does not stop: anything running as your user on this
|
|
981
984
|
machine can set `@hwip_shell` on a session, or run `hwip sh` itself — and
|
|
982
985
|
so can the phone, while a Claude Code session (`!tmux set-option …`) or a
|
|
983
|
-
marked shell is open to it.
|
|
984
|
-
|
|
986
|
+
marked shell is open to it. And the phone can always open a marked shell of
|
|
987
|
+
its own: New session → Terminal starts `hwip-<project>-sh` in any project
|
|
988
|
+
this machine recorded. The marker keeps the phone from typing into panes
|
|
989
|
+
you opened yourself (an agent's, a plain shell's), not from running
|
|
990
|
+
commands on this machine.
|
|
985
991
|
And no layer authenticates the sender: whoever holds your API key, or
|
|
986
992
|
controls the backend, can type into every marked shell and, through `!`,
|
|
987
993
|
every Claude Code session.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* offer it only to a computer that lists this.
|
|
5
5
|
*/
|
|
6
6
|
export declare const SESSION_START_CAPABILITY = "session-start-v1";
|
|
7
|
+
/** A fresh start that names agent kind `shell` opens the project's `hwip sh` Terminal (listener `startShellBeside`). */
|
|
8
|
+
export declare const SESSION_SHELL_CAPABILITY = "session-shell-v1";
|
|
7
9
|
/** The one message both halves of presence ride; the server answers
|
|
8
10
|
* `listener.presence.ack` or `listener.presence.error`. */
|
|
9
11
|
export declare const LISTENER_PRESENCE = "listener.presence";
|
package/dist/device-presence.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createDeviceKeyRegistration = exports.LISTENER_PRESENCE = exports.SESSION_START_CAPABILITY = void 0;
|
|
3
|
+
exports.createDeviceKeyRegistration = exports.LISTENER_PRESENCE = exports.SESSION_SHELL_CAPABILITY = exports.SESSION_START_CAPABILITY = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Per-device E2E (ADR-0007) — put this machine's public key on its device
|
|
6
6
|
* record, so other devices can wrap pushes and files for it.
|
|
@@ -42,6 +42,8 @@ const schedule_js_1 = require("./schedule.js");
|
|
|
42
42
|
* offer it only to a computer that lists this.
|
|
43
43
|
*/
|
|
44
44
|
exports.SESSION_START_CAPABILITY = 'session-start-v1';
|
|
45
|
+
/** A fresh start that names agent kind `shell` opens the project's `hwip sh` Terminal (listener `startShellBeside`). */
|
|
46
|
+
exports.SESSION_SHELL_CAPABILITY = 'session-shell-v1';
|
|
45
47
|
/** The one message both halves of presence ride; the server answers
|
|
46
48
|
* `listener.presence.ack` or `listener.presence.error`. */
|
|
47
49
|
exports.LISTENER_PRESENCE = 'listener.presence';
|
|
@@ -52,7 +54,7 @@ const createDeviceKeyRegistration = (deps) => {
|
|
|
52
54
|
const register = async () => {
|
|
53
55
|
try {
|
|
54
56
|
const publicKey = await deps.publicKey();
|
|
55
|
-
const sent = deps.send({ type: exports.LISTENER_PRESENCE, data: { publicKey, capabilities: [file_crypto_js_1.FILE_CHUNK_CAPABILITY, lan_receiver_js_1.LAN_LARGE_CAPABILITY, schedule_js_1.SCHEDULE_SESSION_CAPABILITY, exports.SESSION_START_CAPABILITY] } });
|
|
57
|
+
const sent = deps.send({ type: exports.LISTENER_PRESENCE, data: { publicKey, capabilities: [file_crypto_js_1.FILE_CHUNK_CAPABILITY, lan_receiver_js_1.LAN_LARGE_CAPABILITY, schedule_js_1.SCHEDULE_SESSION_CAPABILITY, exports.SESSION_START_CAPABILITY, exports.SESSION_SHELL_CAPABILITY] } });
|
|
56
58
|
if (!sent)
|
|
57
59
|
deps.log('device key: not registered — no connection right now, retried at the next open');
|
|
58
60
|
return sent;
|
package/dist/installer.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export declare const failedStepCount: () => number;
|
|
|
28
28
|
export declare const reportDetectedAgents: (agents: Agent[]) => void;
|
|
29
29
|
/** The install's last line. Exported for tests. */
|
|
30
30
|
export declare const closingLine: (failed: number) => string;
|
|
31
|
+
/** Where the install's last screen sends a new user for the phone app. `ct`/`utm_source` name this surface. */
|
|
32
|
+
export declare const APP_STORE_URL = "https://apps.apple.com/app/id6818053606?ct=cli-install";
|
|
33
|
+
export declare const PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=app.hwip&referrer=utm_source%3Dcli-install";
|
|
31
34
|
/** Exported for tests (installGemini shells out to the real `gemini` binary,
|
|
32
35
|
* so the merge semantics can't be covered through it deterministically). */
|
|
33
36
|
export declare const mergeJsonFile: (filePath: string, patch: Record<string, unknown>) => void;
|
package/dist/installer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleInstall = exports.serviceInstallChoice = exports.collectCredentials = exports.unknownOnlyIds = exports.filterAgentsByIds = exports.AGENT_INSTALLERS = exports.firstStderrLine = exports.injectCodexMcpEntry = exports.injectMcpEntry = exports.addAiderReadDirective = exports.writeManagedRule = exports.mergeJsonFile = exports.closingLine = exports.reportDetectedAgents = exports.failedStepCount = exports.failMerge = exports.mergeFailureLines = exports.shouldReauth = void 0;
|
|
3
|
+
exports.handleInstall = exports.serviceInstallChoice = exports.collectCredentials = exports.unknownOnlyIds = exports.filterAgentsByIds = exports.AGENT_INSTALLERS = exports.firstStderrLine = exports.injectCodexMcpEntry = exports.injectMcpEntry = exports.addAiderReadDirective = exports.writeManagedRule = exports.mergeJsonFile = exports.PLAY_STORE_URL = exports.APP_STORE_URL = exports.closingLine = exports.reportDetectedAgents = exports.failedStepCount = exports.failMerge = exports.mergeFailureLines = exports.shouldReauth = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
@@ -136,6 +136,9 @@ const closingLine = (failed) => {
|
|
|
136
136
|
return `Done, but ${steps} you: see the lines marked "-" above. Then restart your agents.`;
|
|
137
137
|
};
|
|
138
138
|
exports.closingLine = closingLine;
|
|
139
|
+
/** Where the install's last screen sends a new user for the phone app. `ct`/`utm_source` name this surface. */
|
|
140
|
+
exports.APP_STORE_URL = 'https://apps.apple.com/app/id6818053606?ct=cli-install';
|
|
141
|
+
exports.PLAY_STORE_URL = 'https://play.google.com/store/apps/details?id=app.hwip&referrer=utm_source%3Dcli-install';
|
|
139
142
|
/** An edit that would lose a comment of the user's names the file, like every other refusal. */
|
|
140
143
|
const namingFile = (filePath, edit) => {
|
|
141
144
|
try {
|
|
@@ -837,6 +840,8 @@ const handleInstall = async (args) => {
|
|
|
837
840
|
console.log('\n Testing connection...');
|
|
838
841
|
await testConnection(apiKey, baseUrl);
|
|
839
842
|
console.log(`\n ${(0, exports.closingLine)(failedSteps)}\n`);
|
|
843
|
+
console.log(` Get the app: ${exports.APP_STORE_URL}`);
|
|
844
|
+
console.log(` Android: ${exports.PLAY_STORE_URL}\n`);
|
|
840
845
|
// The single most-reported surprise: pushes are not scoped to `hwip cc`.
|
|
841
846
|
// Hooks fire for every session of every configured agent, so say it here —
|
|
842
847
|
// at the moment the hooks get installed — together with the volume dials.
|
package/dist/listener.d.ts
CHANGED
|
@@ -549,6 +549,8 @@ export interface SessionResumeResult {
|
|
|
549
549
|
error?: string;
|
|
550
550
|
at: string;
|
|
551
551
|
}
|
|
552
|
+
/** The `agentKind` a fresh start names to ask for a Terminal (`hwip sh`) instead of an agent (`SESSION_SHELL_CAPABILITY`). */
|
|
553
|
+
export declare const SHELL_START_KIND = "shell";
|
|
552
554
|
/**
|
|
553
555
|
* Start a remembered session again, detached, and tell the phone what happened.
|
|
554
556
|
* Returns true when the message was ours to answer.
|
package/dist/listener.js
CHANGED
|
@@ -26,8 +26,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
28
|
exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.withSettledPrompts = exports.PROMPT_SEAL_WAIT_MS = exports.withBlockedPrompts = exports.setPromptSealer = exports.deriveSessionState = exports.resetSessionStates = exports.continueRuns = exports.runIdOfEpoch = exports.__checkoutCacheSize = exports.parseSessionName = exports.resetPaneFitClaims = exports.tmuxTargetFor = exports.subagentTranscriptFor = exports.recordTargets = exports.recordSubagentTranscripts = exports.isSubagentSessionName = exports.detectRemoteAgent = exports.sessionLiveness = exports.sessionExists = exports.liveSessionNames = exports.tmuxArgs = exports.silenceListenerLog = exports.invalidateTmuxSocketCache = exports.wheelInput = exports.wheelEvents = exports.WHEEL_STATE_FORMAT = exports.resolveWheel = exports.resolveKeys = exports.probePane = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.commandsReportDue = exports.commandsFingerprint = exports.COMMAND_SCAN_INTERVAL_MS = exports.sessionsReportDue = exports.sessionsFingerprint = exports.withRunSeqs = exports.staleConnectionAction = exports.askedToReconnect = exports.MAX_STALE_CONNECTION_DROPS = exports.createReportAckWatch = exports.REPORT_ACK_TIMEOUT_MS = exports.SESSION_REPORT_HEARTBEAT_MS = exports.SESSION_REPORT_INTERVAL_MS = exports.heartbeatVerdict = exports.pingDue = exports.PING_IDLE_MS = void 0;
|
|
29
|
-
exports.
|
|
30
|
-
exports.handleListener = exports.liveScheduleRunnerDeps = exports.listenerSettings = exports.resolveWsUrl = exports.streamSession = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.displayFolder = exports.claimLanFile = exports.saveFileFromS3 = exports.downloadSealedEach = exports.answerFileBound = exports.downloadAttachments = exports.setAttachmentContext = exports.shellTextRefusal = exports.writeRemoteMarker = exports.isInventoried = exports.syncTmuxAgentNames = void 0;
|
|
29
|
+
exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.fitSealedFrame = exports.SEALED_FRAME_MAX_BYTES = exports.stopAllStreams = exports.stopStream = exports.stopStreamOf = exports.handleTurnWatch = exports.sealTurnBatch = exports.resolveWatchTranscript = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_RENEW_FRESH_MS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.hasFrameBudget = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.STREAM_CAPTURE_LINES = exports.scheduleInjectSnapshots = exports.historyLinesIn = exports.cursorLineFor = exports.parsePaneGeometry = exports.STREAM_GEOMETRY_FORMAT = exports.handleScreenHistoryRequest = exports.SCREEN_HISTORY_MAX_LINES = exports.handleSessionForgetRequest = exports.handleSessionExitRequest = exports.SESSION_EXIT_STEP_MS = exports.handleSessionResumeRequest = exports.SHELL_START_KIND = exports.handleScheduleListRequest = exports.handleFileBlobRequest = exports.handleFileTreeRequest = exports.handleDiffFileRequest = exports.handleDiffFilesRequest = exports.fileViewBytes = exports.handleScreenRequest = exports.setSubscriberTrust = void 0;
|
|
30
|
+
exports.handleListener = exports.liveScheduleRunnerDeps = exports.listenerSettings = exports.resolveWsUrl = exports.streamSession = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.displayFolder = exports.claimLanFile = exports.saveFileFromS3 = exports.downloadSealedEach = exports.answerFileBound = exports.downloadAttachments = exports.setAttachmentContext = exports.shellTextRefusal = exports.writeRemoteMarker = exports.isInventoried = exports.syncTmuxAgentNames = exports.recordInventory = void 0;
|
|
31
31
|
const child_process_1 = require("child_process");
|
|
32
32
|
const crypto_1 = require("crypto");
|
|
33
33
|
const fs_1 = require("fs");
|
|
@@ -1466,6 +1466,38 @@ const conversationRunning = (known, live) => {
|
|
|
1466
1466
|
return current?.agentKind === known.agentKind && current.agentSessionId === id;
|
|
1467
1467
|
});
|
|
1468
1468
|
};
|
|
1469
|
+
/** The `agentKind` a fresh start names to ask for a Terminal (`hwip sh`) instead of an agent (`SESSION_SHELL_CAPABILITY`). */
|
|
1470
|
+
exports.SHELL_START_KIND = 'shell';
|
|
1471
|
+
/**
|
|
1472
|
+
* A Terminal in `sessionName`'s project (hwip docs/session-runs.md Q50): `hwip sh --detach` in the directory this machine recorded for it — the login
|
|
1473
|
+
* shell the phone may type into (`@hwip_shell`), audited as every shell input is. The phone names a session it saw, never
|
|
1474
|
+
* a directory, as for an agent; the newest run of the name gives the directory, whichever agent ran it.
|
|
1475
|
+
*/
|
|
1476
|
+
const startShellBeside = (sessionName, reply) => {
|
|
1477
|
+
const known = (0, session_registry_js_1.recallSession)(sessionName, Date.now());
|
|
1478
|
+
if (!known) {
|
|
1479
|
+
(0, listener_log_js_1.log)(`✗ shell beside ${sessionName}: never seen on this machine`);
|
|
1480
|
+
return reply({ error: 'unknown_session' });
|
|
1481
|
+
}
|
|
1482
|
+
if (!(0, session_registry_js_1.sessionDirectoryExists)(known)) {
|
|
1483
|
+
(0, listener_log_js_1.log)(`✗ shell beside ${sessionName}: ${known.cwd} is gone`);
|
|
1484
|
+
return reply({ error: 'missing_directory' });
|
|
1485
|
+
}
|
|
1486
|
+
// One shell per project: a repeat tap reopens it, so this bucket only stops a flood of starts.
|
|
1487
|
+
if (!(0, exports.checkRateLimit)(`start:shell:${known.cwd}`, undefined, FRESH_START_COST))
|
|
1488
|
+
return reply({ error: 'rate_limited' });
|
|
1489
|
+
const shell = process.env.SHELL || (0, os_1.userInfo)().shell || '/bin/sh';
|
|
1490
|
+
const started = (0, wrapper_js_1.startShellSession)(known.cwd, shell, (args) => {
|
|
1491
|
+
const r = (0, child_process_1.spawnSync)('tmux', (0, tmux_socket_js_1.tmuxArgs)(args), { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1492
|
+
return { status: r.error ? null : r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
|
|
1493
|
+
});
|
|
1494
|
+
if ('refused' in started) {
|
|
1495
|
+
(0, listener_log_js_1.log)(`✗ shell beside ${sessionName}: ${started.refused}`);
|
|
1496
|
+
return reply({ error: started.refused });
|
|
1497
|
+
}
|
|
1498
|
+
(0, listener_log_js_1.log)(`+ shell ${started.session} in ${known.cwd}`);
|
|
1499
|
+
return reply({ resumed: true, sessionName: started.session });
|
|
1500
|
+
};
|
|
1469
1501
|
/**
|
|
1470
1502
|
* Start a remembered session again, detached, and tell the phone what happened.
|
|
1471
1503
|
* Returns true when the message was ours to answer.
|
|
@@ -1505,6 +1537,8 @@ const handleSessionResumeRequest = (req, send) => {
|
|
|
1505
1537
|
// through whichever path is cheaper.
|
|
1506
1538
|
if (!(0, exports.checkRateLimit)(sessionName, undefined, SUBMIT_COST))
|
|
1507
1539
|
return reply({ error: 'rate_limited' });
|
|
1540
|
+
if (req.fresh === true && req.agentKind === exports.SHELL_START_KIND)
|
|
1541
|
+
return startShellBeside(sessionName, reply);
|
|
1508
1542
|
// A named agent is answered with that run or not at all. Falling back to the
|
|
1509
1543
|
// newest run would start claude for a phone that asked for pi — silently,
|
|
1510
1544
|
// which is the exact failure this path exists to remove. A phone that names
|
|
@@ -5285,7 +5319,7 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
5285
5319
|
// already exists rather than a request of their own: the phone is
|
|
5286
5320
|
// already reading this device record (recent-projects.ts).
|
|
5287
5321
|
const reported = (0, exports.withRunSeqs)(sessions);
|
|
5288
|
-
const recent = (0, recent_projects_js_1.recentProjectsToReport)();
|
|
5322
|
+
const recent = (0, recent_projects_js_1.recentProjectsToReport)(new Set(sessions.map((s) => s.name)));
|
|
5289
5323
|
const fingerprint = `${(0, exports.sessionsFingerprint)(reported)}␞${recent ? JSON.stringify(recent) : '-'}`;
|
|
5290
5324
|
if (!(0, exports.sessionsReportDue)(fingerprint, lastSentFingerprint, lastSentAtMs, Date.now())) {
|
|
5291
5325
|
return;
|
|
@@ -17,15 +17,21 @@ export interface RecentProject {
|
|
|
17
17
|
}
|
|
18
18
|
/** A short list: the projects a person works in lately, not every folder the machine ever ran one in. */
|
|
19
19
|
export declare const RECENT_PROJECTS_MAX = 12;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Each project's agents, from the newest session of each, in the order `known` gives (newest first).
|
|
22
|
+
*
|
|
23
|
+
* A project with a session in `live` is listed without counting toward the 12: a running session's entry is always the
|
|
24
|
+
* newest, so twelve running projects would leave no room for one the machine ran before (hwip docs/session-runs.md Q50).
|
|
25
|
+
* Its entries still go out — the agents it ran before and does not run now are only here.
|
|
26
|
+
*/
|
|
21
27
|
export declare const recentProjects: (known: readonly {
|
|
22
28
|
name: string;
|
|
23
29
|
agentKind: string;
|
|
24
30
|
project?: string;
|
|
25
|
-
}[]) => RecentProject[];
|
|
31
|
+
}[], live?: ReadonlySet<string>) => RecentProject[];
|
|
26
32
|
/**
|
|
27
|
-
* The list for this report, or undefined when the registry would not read — an empty list would
|
|
33
|
+
* The list for this report beside `live`, the sessions it reports running, or undefined when the registry would not read — an empty list would
|
|
28
34
|
* clear what the server holds, and the phone would lose every project to start in.
|
|
29
35
|
*/
|
|
30
|
-
export declare const recentProjectsToReport: (now?: number) => RecentProject[] | undefined;
|
|
36
|
+
export declare const recentProjectsToReport: (live: ReadonlySet<string>, now?: number) => RecentProject[] | undefined;
|
|
31
37
|
//# sourceMappingURL=recent-projects.d.ts.map
|
package/dist/recent-projects.js
CHANGED
|
@@ -18,19 +18,27 @@ exports.RECENT_PROJECTS_MAX = 12;
|
|
|
18
18
|
const isSubagent = (name) => /\.\d+$/.test(name);
|
|
19
19
|
/** A session a schedule started: starting from it would copy the scheduled run, not the project. */
|
|
20
20
|
const isScheduled = (name) => name.includes('-sched-');
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Each project's agents, from the newest session of each, in the order `known` gives (newest first).
|
|
23
|
+
*
|
|
24
|
+
* A project with a session in `live` is listed without counting toward the 12: a running session's entry is always the
|
|
25
|
+
* newest, so twelve running projects would leave no room for one the machine ran before (hwip docs/session-runs.md Q50).
|
|
26
|
+
* Its entries still go out — the agents it ran before and does not run now are only here.
|
|
27
|
+
*/
|
|
28
|
+
const recentProjects = (known, live = new Set()) => {
|
|
29
|
+
const projectOf = (e) => e.project || e.name;
|
|
30
|
+
const eligible = known.filter((e) => !isSubagent(e.name) && !isScheduled(e.name));
|
|
31
|
+
// As the phone counts running: a scheduled run or a subagent is no running session of the project there.
|
|
32
|
+
const running = new Set(eligible.filter((e) => live.has(e.name)).map(projectOf));
|
|
33
|
+
const past = new Set();
|
|
24
34
|
const seen = new Set();
|
|
25
35
|
const listed = [];
|
|
26
|
-
for (const e of
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!projects.has(project)) {
|
|
31
|
-
if (projects.size >= exports.RECENT_PROJECTS_MAX)
|
|
36
|
+
for (const e of eligible) {
|
|
37
|
+
const project = projectOf(e);
|
|
38
|
+
if (!running.has(project) && !past.has(project)) {
|
|
39
|
+
if (past.size >= exports.RECENT_PROJECTS_MAX)
|
|
32
40
|
continue;
|
|
33
|
-
|
|
41
|
+
past.add(project);
|
|
34
42
|
}
|
|
35
43
|
const key = `${project}\u0000${e.agentKind}`;
|
|
36
44
|
if (seen.has(key))
|
|
@@ -42,8 +50,8 @@ const recentProjects = (known) => {
|
|
|
42
50
|
};
|
|
43
51
|
exports.recentProjects = recentProjects;
|
|
44
52
|
/**
|
|
45
|
-
* The list for this report, or undefined when the registry would not read — an empty list would
|
|
53
|
+
* The list for this report beside `live`, the sessions it reports running, or undefined when the registry would not read — an empty list would
|
|
46
54
|
* clear what the server holds, and the phone would lose every project to start in.
|
|
47
55
|
*/
|
|
48
|
-
const recentProjectsToReport = (now = Date.now()) => (0, session_registry_js_1.registryUnreadable)() ? undefined : (0, exports.recentProjects)((0, session_registry_js_1.knownSessions)(now));
|
|
56
|
+
const recentProjectsToReport = (live, now = Date.now()) => ((0, session_registry_js_1.registryUnreadable)() ? undefined : (0, exports.recentProjects)((0, session_registry_js_1.knownSessions)(now), live));
|
|
49
57
|
exports.recentProjectsToReport = recentProjectsToReport;
|
package/dist/transcript-tail.js
CHANGED
|
@@ -507,6 +507,8 @@ const contentBlocks = (entry) => {
|
|
|
507
507
|
const SYNTHETIC_BLOCKS = /<(task-notification|system-reminder|local-command-caveat|local-command-stdout|command-message|command-name|command-args)>[\s\S]*?<\/\1>/g;
|
|
508
508
|
/** `<command-name>/simplify</command-name>` — the one wrapper worth keeping, as what it names. */
|
|
509
509
|
const COMMAND_NAME = /<command-name>([^<]*)<\/command-name>/;
|
|
510
|
+
/** `<command-args>prompt run</command-args>` — what the person typed after the command name. */
|
|
511
|
+
const COMMAND_ARGS = /<command-args>([\s\S]*?)<\/command-args>/;
|
|
510
512
|
/**
|
|
511
513
|
* Whether this entry is a person speaking.
|
|
512
514
|
*
|
|
@@ -577,10 +579,12 @@ const typedTextOf = (entry) => {
|
|
|
577
579
|
* separate feature, not a reason to ship the path.
|
|
578
580
|
*/
|
|
579
581
|
const stripAttachmentMarkers = (text) => {
|
|
580
|
-
const
|
|
582
|
+
const name = text.match(COMMAND_NAME)?.[1]?.trim();
|
|
583
|
+
const args = text.match(COMMAND_ARGS)?.[1]?.trim();
|
|
581
584
|
const body = text.replace(SYNTHETIC_BLOCKS, '');
|
|
582
|
-
// A slash command is a person's turn, and its name
|
|
583
|
-
// said — the wrapper around
|
|
585
|
+
// A slash command is a person's turn, and its name and arguments are the
|
|
586
|
+
// whole of what they said — the wrapper around them is not.
|
|
587
|
+
const command = name && args ? `${name} ${args}` : name;
|
|
584
588
|
const withCommand = command ? `${command}\n${body}` : body;
|
|
585
589
|
return withCommand.replace(/\[Image:[^\]]*\]/g, '').replace(/\n{3,}/g, '\n\n');
|
|
586
590
|
};
|
package/dist/wrapper.d.ts
CHANGED
|
@@ -210,7 +210,7 @@ export declare const handleAgentSession: (agent: RemoteAgent, extra?: string[],
|
|
|
210
210
|
/** What tmux holds under a `hwip sh` session name. */
|
|
211
211
|
export type ShellSessionState = 'none' | 'marked' | 'unmarked';
|
|
212
212
|
/** A tmux call, injectable for tests. */
|
|
213
|
-
type RunTmux = (args: string[]) => {
|
|
213
|
+
export type RunTmux = (args: string[]) => {
|
|
214
214
|
status: number | null;
|
|
215
215
|
stdout: string;
|
|
216
216
|
stderr?: string;
|
|
@@ -258,6 +258,16 @@ export declare const shellTargets: (shell: string, opts: AgentLaunchOptions, inT
|
|
|
258
258
|
create: SpawnTarget;
|
|
259
259
|
attach: SpawnTarget | null;
|
|
260
260
|
};
|
|
261
|
+
/**
|
|
262
|
+
* The phone's Terminal start (hwip docs/session-runs.md Q50): `hwip sh --detach` run in `projectDir` — the same
|
|
263
|
+
* `hwip-<project>-sh` login shell, marked so the phone may type into it. One already open under that name is the answer
|
|
264
|
+
* as it is; a session of that name `hwip sh` did not mark is refused, never taken over.
|
|
265
|
+
*/
|
|
266
|
+
export declare const startShellSession: (projectDir: string, shell: string, run?: RunTmux) => {
|
|
267
|
+
session: string;
|
|
268
|
+
} | {
|
|
269
|
+
refused: "already_running" | "start_failed";
|
|
270
|
+
};
|
|
261
271
|
/**
|
|
262
272
|
* `hwip sh [--label x] [--detach]` — open (or reattach) `hwip-<project>-sh`
|
|
263
273
|
* running the login shell, marked so the phone may type into it. The label
|
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.familyNames = exports.firstFreeSession = exports.tmuxSessionName = exports.installedTmuxVersion = exports.agentLaunchArgs = exports.agentLaunchEnv = exports.herdrEnvArgs = exports.herdrSessionArgs = exports.sessionSidecarArgs = exports.detectProjectName = exports.detectProjectDir = exports.safeBasename = void 0;
|
|
3
|
+
exports.handleShellSession = exports.startShellSession = 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.familyNames = exports.firstFreeSession = exports.tmuxSessionName = exports.installedTmuxVersion = exports.agentLaunchArgs = exports.agentLaunchEnv = 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
|
|
@@ -736,7 +736,7 @@ exports.readShellSessionState = readShellSessionState;
|
|
|
736
736
|
* run is a detached one, as `--detach` is.
|
|
737
737
|
*/
|
|
738
738
|
const shellTargets = (shell, opts, inTmux = !!process.env.TMUX) => {
|
|
739
|
-
const create = (0, exports.targetForAgent)(shell, ['-l'], 'shell', { label: opts.label ?? 'sh', detach: true });
|
|
739
|
+
const create = (0, exports.targetForAgent)(shell, ['-l'], 'shell', { projectDir: opts.projectDir, label: opts.label ?? 'sh', detach: true });
|
|
740
740
|
const session = create.session ?? '';
|
|
741
741
|
// `attach-session`, never `new -A`: a session that died since the probe
|
|
742
742
|
// must not come back as a fresh, unmarked one.
|
|
@@ -744,6 +744,22 @@ const shellTargets = (shell, opts, inTmux = !!process.env.TMUX) => {
|
|
|
744
744
|
return { create, attach: opts.detach || inTmux ? null : attach };
|
|
745
745
|
};
|
|
746
746
|
exports.shellTargets = shellTargets;
|
|
747
|
+
/**
|
|
748
|
+
* The phone's Terminal start (hwip docs/session-runs.md Q50): `hwip sh --detach` run in `projectDir` — the same
|
|
749
|
+
* `hwip-<project>-sh` login shell, marked so the phone may type into it. One already open under that name is the answer
|
|
750
|
+
* as it is; a session of that name `hwip sh` did not mark is refused, never taken over.
|
|
751
|
+
*/
|
|
752
|
+
const startShellSession = (projectDir, shell, run = runTmux) => {
|
|
753
|
+
const { create } = (0, exports.shellTargets)(shell, { projectDir, detach: true }, true);
|
|
754
|
+
const session = create.session ?? '';
|
|
755
|
+
const plan = (0, exports.planShellSession)(create, null, (0, exports.readShellSessionState)(session, run));
|
|
756
|
+
if (plan.kind === 'refuse')
|
|
757
|
+
return { refused: 'already_running' };
|
|
758
|
+
if (plan.kind === 'create' && run(plan.args).status !== 0)
|
|
759
|
+
return { refused: 'start_failed' };
|
|
760
|
+
return { session };
|
|
761
|
+
};
|
|
762
|
+
exports.startShellSession = startShellSession;
|
|
747
763
|
/**
|
|
748
764
|
* `hwip sh [--label x] [--detach]` — open (or reattach) `hwip-<project>-sh`
|
|
749
765
|
* running the login shell, marked so the phone may type into it. The label
|