@hwip/cli 0.1.3 → 0.1.5
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 +49 -18
- package/dist/agent-dirs.d.ts +15 -0
- package/dist/agent-dirs.js +76 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +12 -1
- package/dist/codex-config.d.ts +8 -5
- package/dist/codex-config.js +121 -12
- package/dist/command-scan.d.ts +7 -3
- package/dist/command-scan.js +59 -15
- package/dist/installer.js +7 -1
- package/dist/listener.d.ts +33 -0
- package/dist/listener.js +72 -23
- package/dist/mcp-command.d.ts +14 -2
- package/dist/mcp-command.js +18 -5
- package/dist/pane-fit.d.ts +39 -11
- package/dist/pane-fit.js +162 -29
- package/dist/remote-agents.d.ts +64 -12
- package/dist/remote-agents.js +141 -18
- package/dist/schedule-command.d.ts +11 -1
- package/dist/schedule-command.js +113 -4
- package/dist/schedule-launch.d.ts +3 -1
- package/dist/schedule-launch.js +4 -1
- package/dist/schedule-remote.d.ts +7 -1
- package/dist/schedule-remote.js +51 -4
- package/dist/schedule-runner.js +5 -2
- package/dist/schedule.d.ts +31 -0
- package/dist/schedule.js +45 -7
- package/dist/session-registry.d.ts +12 -0
- package/dist/session-registry.js +23 -3
- package/dist/session-title.d.ts +46 -0
- package/dist/session-title.js +103 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +41 -1
- package/dist/usage.js +4 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +33 -8
- package/dist/wrapper.js +10 -0
- package/package.json +1 -1
package/dist/listener.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { type HwipConfig } from './config.js';
|
|
|
24
24
|
import { type ShellAuditEntry } from './shell-audit.js';
|
|
25
25
|
import { type KnownSession } from './session-registry.js';
|
|
26
26
|
import { type AgentKind } from './remote-agents.js';
|
|
27
|
+
import type { ProviderNameSource } from './session-title.js';
|
|
27
28
|
import { type TranscriptSource } from './transcript-tail.js';
|
|
28
29
|
import { type AgentState } from './agent-state.js';
|
|
29
30
|
import { type SealedFileMeta, type SealedV2 } from './crypto.js';
|
|
@@ -121,6 +122,12 @@ interface AgentSession {
|
|
|
121
122
|
* word for both would put the ambiguity in the type.
|
|
122
123
|
*/
|
|
123
124
|
providerSessionName?: string | null;
|
|
125
|
+
/**
|
|
126
|
+
* Who chose `providerSessionName`: a person in the agent (`'user'`) or the
|
|
127
|
+
* agent itself (`'auto'`). Omitted when the agent's store cannot say. The
|
|
128
|
+
* account setting that hides agent-made names hides `'auto'` only.
|
|
129
|
+
*/
|
|
130
|
+
providerSessionNameSource?: ProviderNameSource;
|
|
124
131
|
createdAt?: string;
|
|
125
132
|
/**
|
|
126
133
|
* Which run of the name this is (hwip docs/session-runs.md): tmux
|
|
@@ -222,6 +229,9 @@ export interface ReportedKnownSession {
|
|
|
222
229
|
lastSeq?: number;
|
|
223
230
|
/** The later tmux sessions the run went on in (Q35): the server ends another run's push window at their start. */
|
|
224
231
|
tmuxRunIds?: string[];
|
|
232
|
+
/** The last name the agent gave the run, and who gave it (hwip docs/session-runs.md Q39): what the phone calls it once ended. */
|
|
233
|
+
providerSessionName?: string;
|
|
234
|
+
providerSessionNameSource?: ProviderNameSource;
|
|
225
235
|
lastSeenAt: string;
|
|
226
236
|
}
|
|
227
237
|
/** Each live run's first seq, for the report (Q24). A run that has recorded nothing yet has none. */
|
|
@@ -274,6 +284,8 @@ export declare const resolveKeys: (keys: string[]) => string[] | null;
|
|
|
274
284
|
export { invalidateTmuxSocketCache, silenceListenerLog, tmuxArgs };
|
|
275
285
|
export { liveSessionNames, sessionExists, sessionLiveness };
|
|
276
286
|
export { detectRemoteAgent, isSubagentSessionName, recordSubagentTranscripts, recordTargets, subagentTranscriptFor, tmuxTargetFor };
|
|
287
|
+
/** Test hook. */
|
|
288
|
+
export declare const resetPaneFitClaims: () => void;
|
|
277
289
|
/**
|
|
278
290
|
* Parse a `hwip-*` tmux session name into `{project, label}`. The wrapper
|
|
279
291
|
* emits `hwip-<project>` and, with `--label <x>`, `hwip-<project>-<x>`; the
|
|
@@ -631,6 +643,27 @@ export interface ScreenHistorySnapshot {
|
|
|
631
643
|
error?: string;
|
|
632
644
|
capturedAt: string;
|
|
633
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* What a sealed history page's envelope holds (as JSON): the page and every
|
|
648
|
+
* number a viewer pages by, bound to the request it answers and to the
|
|
649
|
+
* subscription it was sealed for. On a sealed stream the snapshot carries
|
|
650
|
+
* only `requestId` (for routing) beside this — a relay that hands an earlier
|
|
651
|
+
* page back under a new request, or rewrites where it sits, is caught on open.
|
|
652
|
+
*/
|
|
653
|
+
export type SealedHistoryPage = {
|
|
654
|
+
sessionName: string;
|
|
655
|
+
requestId: string;
|
|
656
|
+
nonce?: string;
|
|
657
|
+
epoch: number;
|
|
658
|
+
before: number;
|
|
659
|
+
lines: number;
|
|
660
|
+
hasMore: boolean;
|
|
661
|
+
truncated: boolean;
|
|
662
|
+
/** The pane's history size when this page was captured, so the viewer can
|
|
663
|
+
* tell how much the history grew between its request and the capture. */
|
|
664
|
+
historySize: number;
|
|
665
|
+
content: string;
|
|
666
|
+
};
|
|
634
667
|
/**
|
|
635
668
|
* Answer a phone's request for one page of scrollback above the live window.
|
|
636
669
|
* Returns true when the message was ours to answer, so the caller stops routing
|
package/dist/listener.js
CHANGED
|
@@ -25,9 +25,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.
|
|
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 = void 0;
|
|
28
|
+
exports.setSubscriberTrust = 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.resolveKeys = exports.probePane = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.commandsReportDue = exports.commandsFingerprint = exports.COMMAND_SCAN_INTERVAL_MS = exports.sessionsReportDue = exports.sessionsFingerprint = exports.knownSessionsFingerprint = exports.knownSessionsToReport = exports.withRunSeqs = exports.KNOWN_SESSIONS_REPORTED = 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.syncTmuxAgentNames = exports.recordInventory = 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.handleScheduleListRequest = exports.handleFileBlobRequest = exports.handleFileTreeRequest = exports.handleDiffFileRequest = exports.handleDiffFilesRequest = exports.fileViewBytes = exports.handleScreenRequest = 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 = 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");
|
|
@@ -319,16 +319,18 @@ const knownSessionsToReport = (live, now = Date.now(), seqs = (0, run_seqs_js_1.
|
|
|
319
319
|
...(e.startedAt ? { startedAt: e.startedAt } : {}),
|
|
320
320
|
...(e.runId ? seqs.get((0, run_seqs_js_1.runSeqsKey)(e.name, e.runId)) : undefined),
|
|
321
321
|
...(e.tmuxRunIds?.length ? { tmuxRunIds: e.tmuxRunIds } : {}),
|
|
322
|
+
...(e.providerSessionName ? { providerSessionName: e.providerSessionName } : {}),
|
|
323
|
+
...(e.providerSessionName && e.providerSessionNameSource ? { providerSessionNameSource: e.providerSessionNameSource } : {}),
|
|
322
324
|
lastSeenAt: e.lastSeenAt,
|
|
323
325
|
}));
|
|
324
326
|
};
|
|
325
327
|
exports.knownSessionsToReport = knownSessionsToReport;
|
|
326
|
-
const knownSessionsFingerprint = (known) => known.map((k) => `${k.name}${k.agentKind}${k.runId ?? ''}${k.lastSeenAt}${k.firstSeq ?? ''}${k.lastSeq ?? ''}${k.tmuxRunIds?.join(',') ?? ''}`).sort().join('|');
|
|
328
|
+
const knownSessionsFingerprint = (known) => known.map((k) => `${k.name}${k.agentKind}${k.runId ?? ''}${k.lastSeenAt}${k.firstSeq ?? ''}${k.lastSeq ?? ''}${k.tmuxRunIds?.join(',') ?? ''} ${k.providerSessionNameSource ?? ''} ${k.providerSessionName ?? ''}`).sort().join('|');
|
|
327
329
|
exports.knownSessionsFingerprint = knownSessionsFingerprint;
|
|
328
330
|
const sessionsFingerprint = (sessions) => sessions
|
|
329
331
|
.map((s) => JSON.stringify([
|
|
330
332
|
s.name, s.attached, s.agentKind, s.agentSessionId ?? null,
|
|
331
|
-
s.project, s.label ?? null, s.providerSessionName ?? null, s.createdAt ?? null, s.runId ?? null, s.firstSeq ?? null,
|
|
333
|
+
s.project, s.label ?? null, s.providerSessionName ?? null, s.providerSessionNameSource ?? null, s.createdAt ?? null, s.runId ?? null, s.firstSeq ?? null,
|
|
332
334
|
s.lastActivityAt ?? null, s.state ?? null,
|
|
333
335
|
s.stateChangedAt ?? null, s.stateRuleId ?? null,
|
|
334
336
|
s.blockedPrompt?.id ?? null, s.blockedPrompt?.at ?? null,
|
|
@@ -588,8 +590,13 @@ const worktreeCleanup = (0, listener_worktree_cleanup_js_1.createWorktreeCleanup
|
|
|
588
590
|
notify: desktop_notify_js_1.notifyDesktop,
|
|
589
591
|
tmux: (args, timeoutMs) => (0, worktree_js_1.tmuxRun)((0, tmux_socket_js_1.tmuxArgs)(args), timeoutMs),
|
|
590
592
|
});
|
|
591
|
-
/** Sizes a watched
|
|
592
|
-
const paneFitter = (0, pane_fit_js_1.createPaneFitter)(runTmux);
|
|
593
|
+
/** Sizes a watched window to its viewer while the phone is its latest user (pane-fit.ts). */
|
|
594
|
+
const paneFitter = (0, pane_fit_js_1.createPaneFitter)(runTmux, undefined, (0, pane_fit_js_1.macosLastKeyAt)());
|
|
595
|
+
/** Test hook. */
|
|
596
|
+
const resetPaneFitClaims = () => paneFitter.forgetClaims();
|
|
597
|
+
exports.resetPaneFitClaims = resetPaneFitClaims;
|
|
598
|
+
/** How often fitted windows are checked for a desktop that took them back; idle windows cost nothing. */
|
|
599
|
+
const PANE_FIT_RECHECK_MS = 1_000;
|
|
593
600
|
// ─── Session inventory ──────────────────────────────────────────────
|
|
594
601
|
/**
|
|
595
602
|
* Parse a `hwip-*` tmux session name into `{project, label}`. The wrapper
|
|
@@ -1745,11 +1752,11 @@ const handleScreenHistoryRequest = (req, send) => {
|
|
|
1745
1752
|
return reply({ error: 'capture_failed' });
|
|
1746
1753
|
// Everything below this line the caller already has.
|
|
1747
1754
|
const held = before;
|
|
1748
|
-
// Scrolled past the oldest line
|
|
1749
|
-
// view stops, and stopping is not a failure.
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1755
|
+
// Scrolled past the oldest line: an empty page rather than an error — the
|
|
1756
|
+
// view stops, and stopping is not a failure.
|
|
1757
|
+
const captured = depth <= held
|
|
1758
|
+
? { content: '', truncated: false }
|
|
1759
|
+
: capturePaneRange(req.sessionName, -(held + lines), -(held + 1));
|
|
1753
1760
|
if (!captured)
|
|
1754
1761
|
return reply({ error: 'capture_failed' });
|
|
1755
1762
|
const returned = countLines(captured.content);
|
|
@@ -1761,11 +1768,21 @@ const handleScreenHistoryRequest = (req, send) => {
|
|
|
1761
1768
|
};
|
|
1762
1769
|
if (!stream.subscriberPublicKey)
|
|
1763
1770
|
return reply({ ...page, content: captured.content });
|
|
1764
|
-
// Encrypted stream: the page
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1767
|
-
|
|
1768
|
-
|
|
1771
|
+
// Encrypted stream: the page and its numbers ride ONLY inside the envelope
|
|
1772
|
+
// — the empty last page too, or a relay could end the paging with one — and
|
|
1773
|
+
// a failed seal is an error rather than a plaintext page, the same rule the
|
|
1774
|
+
// frame path follows when its encrypt fails.
|
|
1775
|
+
const sealed = {
|
|
1776
|
+
sessionName: req.sessionName,
|
|
1777
|
+
requestId: req.requestId,
|
|
1778
|
+
...(stream.nonce ? { nonce: stream.nonce } : {}),
|
|
1779
|
+
epoch: stream.epoch,
|
|
1780
|
+
...page,
|
|
1781
|
+
historySize: depth,
|
|
1782
|
+
content: captured.content,
|
|
1783
|
+
};
|
|
1784
|
+
(0, crypto_js_1.sealV2)('screen.history', JSON.stringify(sealed), stream.subscriberPublicKey, req.sessionName)
|
|
1785
|
+
.then((encrypted) => send({ ...base, encrypted }))
|
|
1769
1786
|
.catch((err) => {
|
|
1770
1787
|
(0, listener_log_js_1.log)(`⧉ history ${req.sessionName}: seal failed (${err instanceof Error ? err.message : err})`);
|
|
1771
1788
|
send({ ...base, error: 'encrypt_failed' });
|
|
@@ -2076,9 +2093,9 @@ const leaseFor = (renewing) => (renewing ? exports.STREAM_LEASE_MS : STREAM_MAX_
|
|
|
2076
2093
|
* Live mirrors, one per (session, viewing device): every viewer keeps the whole of the machinery a
|
|
2077
2094
|
* single viewer had — its own capture loop state, lease, nonce, epoch and seq, sealed frames — so
|
|
2078
2095
|
* two devices watching one pane do not take each other's stream over. A viewer with no device id
|
|
2079
|
-
* (a web tab) shares the empty slot, one at a time, as before. The tmux pane is shared:
|
|
2080
|
-
* is whatever the last viewer to send input or `cols` asked for
|
|
2081
|
-
* receives the pane at that width.
|
|
2096
|
+
* (a web tab) shares the empty slot, one at a time, as before. The tmux pane is shared: while the
|
|
2097
|
+
* phone is its latest user its width is whatever the last viewer to send input or `cols` asked for,
|
|
2098
|
+
* and a desktop client used since takes it back (pane-fit.ts); every viewer receives the pane at that width.
|
|
2082
2099
|
*/
|
|
2083
2100
|
const activeStreams = new Map();
|
|
2084
2101
|
const streamKey = (sessionName, deviceId) => `${sessionName}\u0000${typeof deviceId === 'string' ? deviceId : ''}`;
|
|
@@ -2607,6 +2624,9 @@ const handleStreamControl = (req, send) => {
|
|
|
2607
2624
|
const renewTarget = (0, tmux_pane_js_1.tmuxTargetFor)(req.sessionName);
|
|
2608
2625
|
if (renewTarget !== null && (colsChanged || streamsOf(req.sessionName).length === 1))
|
|
2609
2626
|
paneFitter.fit(req.sessionName, renewTarget, req.cols);
|
|
2627
|
+
// Otherwise keep the width the pane has, but still notice a desktop client used since (pane-fit.ts).
|
|
2628
|
+
else
|
|
2629
|
+
paneFitter.recheck(req.sessionName);
|
|
2610
2630
|
return true;
|
|
2611
2631
|
}
|
|
2612
2632
|
// Addressed to us but we hold no such stream: it was reaped (lost
|
|
@@ -2648,6 +2668,8 @@ const handleStreamControl = (req, send) => {
|
|
|
2648
2668
|
return true;
|
|
2649
2669
|
}
|
|
2650
2670
|
const fitTarget = (0, tmux_pane_js_1.tmuxTargetFor)(sessionName);
|
|
2671
|
+
// Opening the mirror (or resubscribing) takes the width only from an idle desktop (pane-fit.ts).
|
|
2672
|
+
paneFitter.open(sessionName);
|
|
2651
2673
|
if (fitTarget !== null)
|
|
2652
2674
|
paneFitter.fit(sessionName, fitTarget, req.cols);
|
|
2653
2675
|
let lastContent = null;
|
|
@@ -3047,6 +3069,23 @@ const deliverInput = (input) => {
|
|
|
3047
3069
|
// Only a reason that does not depend on the text: this input may be sealed.
|
|
3048
3070
|
if (!injected)
|
|
3049
3071
|
input.send(streamErrorFrame(input.sessionName, 'input_rejected', input, reason && (0, command_refusal_js_1.isSessionStateRefusal)(reason) ? reason : undefined));
|
|
3072
|
+
else
|
|
3073
|
+
takeWidthBack(input.sessionName, input.deviceId);
|
|
3074
|
+
};
|
|
3075
|
+
/**
|
|
3076
|
+
* Typing from the phone makes it the session's latest user again (pane-fit.ts): a window a desktop
|
|
3077
|
+
* client took back is sized to the sender's width once more.
|
|
3078
|
+
*/
|
|
3079
|
+
const takeWidthBack = (sessionName, deviceId) => {
|
|
3080
|
+
paneFitter.claim(sessionName);
|
|
3081
|
+
const viewer = activeStreams.get(streamKey(sessionName, deviceId)) ?? streamsOf(sessionName)[0];
|
|
3082
|
+
// Already at the sender's width: the claim is enough, and a keystroke costs no tmux round trip (renew rechecks).
|
|
3083
|
+
if (!viewer || paneFitter.fitted(sessionName, viewer.cols))
|
|
3084
|
+
return;
|
|
3085
|
+
const target = (0, tmux_pane_js_1.tmuxTargetFor)(sessionName);
|
|
3086
|
+
if (target === null)
|
|
3087
|
+
return;
|
|
3088
|
+
paneFitter.fit(sessionName, target, viewer.cols);
|
|
3050
3089
|
};
|
|
3051
3090
|
/** Ceiling on concurrent sender lanes. The lane key includes a relay-stamped
|
|
3052
3091
|
* deviceId, but a relay older than the unconditional stamp lets a sender vary
|
|
@@ -3448,7 +3487,7 @@ const collectSessionsVerbose = () => {
|
|
|
3448
3487
|
const agentSessionId = info.currentPath
|
|
3449
3488
|
? (main.agent.resolveSessionId?.(info.currentPath, info.panePid ?? undefined) ?? null)
|
|
3450
3489
|
: null;
|
|
3451
|
-
const
|
|
3490
|
+
const providerName = info.currentPath
|
|
3452
3491
|
? (main.agent.resolveSessionName?.(info.currentPath, info.panePid ?? undefined) ?? null)
|
|
3453
3492
|
: null;
|
|
3454
3493
|
if (info.currentPath)
|
|
@@ -3466,7 +3505,8 @@ const collectSessionsVerbose = () => {
|
|
|
3466
3505
|
agentSessionId,
|
|
3467
3506
|
project: parsed.project,
|
|
3468
3507
|
label: parsed.label,
|
|
3469
|
-
providerSessionName,
|
|
3508
|
+
providerSessionName: providerName?.name ?? null,
|
|
3509
|
+
...(providerName?.source ? { providerSessionNameSource: providerName.source } : {}),
|
|
3470
3510
|
createdAt: epochToIso(group.created),
|
|
3471
3511
|
runId: (0, exports.runIdOfEpoch)(group.created),
|
|
3472
3512
|
lastActivityAt: epochToIso(group.activity),
|
|
@@ -3611,6 +3651,8 @@ const collectSessionsVerbose = () => {
|
|
|
3611
3651
|
startedAt: s.createdAt,
|
|
3612
3652
|
tmuxRunId: runIdOfIso(s.createdAt),
|
|
3613
3653
|
agentSessionId: ownResumeId(s.name),
|
|
3654
|
+
providerSessionName: s.providerSessionName,
|
|
3655
|
+
providerSessionNameSource: s.providerSessionNameSource,
|
|
3614
3656
|
})));
|
|
3615
3657
|
return { sessions, rejected, targets, subagentTranscripts, recordable };
|
|
3616
3658
|
};
|
|
@@ -4589,7 +4631,7 @@ const openSealedCommand = async (push, deps) => {
|
|
|
4589
4631
|
return { text: opened.body ?? '', sender: senderKey, files: named, keys, ifPrompt, schedule };
|
|
4590
4632
|
};
|
|
4591
4633
|
/**
|
|
4592
|
-
* A phone's request to make or remove a schedule (F8). The decisions are `applyPhoneSchedule`'s; this spends
|
|
4634
|
+
* A phone's request to make, change or remove a schedule (F8). The decisions are `applyPhoneSchedule`'s; this spends
|
|
4593
4635
|
* the session's rate budget the way a typed command does and reports a refusal the way every command does.
|
|
4594
4636
|
* Never types anything. A push no one addressed to this machine is not acted on: it changes files, and a
|
|
4595
4637
|
* push.new reaches every listener of the account.
|
|
@@ -4935,6 +4977,10 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
4935
4977
|
clearInterval(commandsTimer);
|
|
4936
4978
|
commandsTimer = null;
|
|
4937
4979
|
}
|
|
4980
|
+
if (paneFitTimer) {
|
|
4981
|
+
clearInterval(paneFitTimer);
|
|
4982
|
+
paneFitTimer = null;
|
|
4983
|
+
}
|
|
4938
4984
|
if (connectTimer) {
|
|
4939
4985
|
clearTimeout(connectTimer);
|
|
4940
4986
|
connectTimer = null;
|
|
@@ -4981,6 +5027,7 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
4981
5027
|
// rescan changes it. The rescan has its own slow timer — it reads the
|
|
4982
5028
|
// filesystem, so it must never ride the 5 s session loop.
|
|
4983
5029
|
let commandsTimer = null;
|
|
5030
|
+
let paneFitTimer = null;
|
|
4984
5031
|
// Per connection, like the inventory gate above: `sock.send` returning
|
|
4985
5032
|
// is not delivery, and a fingerprint that outlived the socket would let
|
|
4986
5033
|
// one lost frame leave the server's catalog stale until the user next
|
|
@@ -5157,6 +5204,8 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
5157
5204
|
sessionsTimer = setInterval(() => void reportSessions(), exports.SESSION_REPORT_INTERVAL_MS);
|
|
5158
5205
|
void reportCommands();
|
|
5159
5206
|
commandsTimer = setInterval(reportCommands, exports.COMMAND_SCAN_INTERVAL_MS);
|
|
5207
|
+
// A desktop key takes a fitted window back within a second, not at the phone's next renew (5 s).
|
|
5208
|
+
paneFitTimer = setInterval(() => paneFitter.recheckAll(), PANE_FIT_RECHECK_MS);
|
|
5160
5209
|
loopSampleAt = Date.now();
|
|
5161
5210
|
loopMeterTimer = setInterval(() => {
|
|
5162
5211
|
const now = Date.now();
|
package/dist/mcp-command.d.ts
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** What the agents run. `hwip` resolves out of the same global bin dir as `npx`. */
|
|
13
13
|
export declare const MCP_LAUNCH_ARGV: readonly ["hwip", "mcp"];
|
|
14
|
+
/**
|
|
15
|
+
* Turns on the MCP server's `hwip_session_title` (mcp-server config `autoName`),
|
|
16
|
+
* so an agent with no title of its own names its session after its task. Set
|
|
17
|
+
* for Codex, Cursor, Windsurf and Gemini only: Claude Code and opencode title
|
|
18
|
+
* their sessions themselves, and the instruction would cost every turn.
|
|
19
|
+
*/
|
|
20
|
+
export declare const AUTONAME_ENV = "HWIP_AUTONAME";
|
|
14
21
|
/**
|
|
15
22
|
* `mcpServers.hwip` — what the installer writes for Cursor and Windsurf.
|
|
16
23
|
* The env block is deliberate: both spawn the MCP server from a graphical
|
|
@@ -18,13 +25,15 @@ export declare const MCP_LAUNCH_ARGV: readonly ["hwip", "mcp"];
|
|
|
18
25
|
* explicitly rather than left to inheritance.
|
|
19
26
|
*
|
|
20
27
|
* `hwipit/plugin`'s `.mcp.json` holds the same shape but is a HAND-KEPT COPY —
|
|
21
|
-
* it lives in another repo and cannot import this. Change one, change both
|
|
28
|
+
* it lives in another repo and cannot import this. Change one, change both —
|
|
29
|
+
* except AUTONAME_ENV, which the plugin (Claude Code) leaves out on purpose.
|
|
22
30
|
*/
|
|
23
31
|
export declare const MCP_SERVERS_ENTRY: {
|
|
24
32
|
command: "hwip";
|
|
25
33
|
args: "mcp"[];
|
|
26
34
|
env: {
|
|
27
35
|
HWIP_API_KEY: string;
|
|
36
|
+
HWIP_AUTONAME: string;
|
|
28
37
|
};
|
|
29
38
|
};
|
|
30
39
|
/**
|
|
@@ -36,10 +45,13 @@ export declare const MCP_SERVERS_ENTRY: {
|
|
|
36
45
|
*/
|
|
37
46
|
export declare const OPENCODE_MCP_ENTRY: {
|
|
38
47
|
type: string;
|
|
39
|
-
command: ("
|
|
48
|
+
command: ("hwip" | "mcp")[];
|
|
40
49
|
enabled: boolean;
|
|
41
50
|
};
|
|
42
51
|
export declare const GEMINI_MCP_ADD: string;
|
|
43
52
|
/** Older gemini CLIs only accept `mcp add <name> -- <command…>`, with no scope flag. */
|
|
44
53
|
export declare const GEMINI_MCP_ADD_LEGACY: string;
|
|
54
|
+
/** The legacy form without `-e`, last: should a gemini that old refuse `-e`, the tools still load,
|
|
55
|
+
* and `hwip verify` says auto session names are off. */
|
|
56
|
+
export declare const GEMINI_MCP_ADD_BARE: string;
|
|
45
57
|
//# sourceMappingURL=mcp-command.d.ts.map
|
package/dist/mcp-command.js
CHANGED
|
@@ -11,10 +11,17 @@
|
|
|
11
11
|
* itself. Everything below is derived from MCP_LAUNCH_ARGV.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.GEMINI_MCP_ADD_LEGACY = exports.GEMINI_MCP_ADD = exports.OPENCODE_MCP_ENTRY = exports.MCP_SERVERS_ENTRY = exports.MCP_LAUNCH_ARGV = void 0;
|
|
14
|
+
exports.GEMINI_MCP_ADD_BARE = exports.GEMINI_MCP_ADD_LEGACY = exports.GEMINI_MCP_ADD = exports.OPENCODE_MCP_ENTRY = exports.MCP_SERVERS_ENTRY = exports.AUTONAME_ENV = exports.MCP_LAUNCH_ARGV = void 0;
|
|
15
15
|
/** What the agents run. `hwip` resolves out of the same global bin dir as `npx`. */
|
|
16
16
|
exports.MCP_LAUNCH_ARGV = ['hwip', 'mcp'];
|
|
17
17
|
const [MCP_BIN, ...MCP_SUBCOMMAND] = exports.MCP_LAUNCH_ARGV;
|
|
18
|
+
/**
|
|
19
|
+
* Turns on the MCP server's `hwip_session_title` (mcp-server config `autoName`),
|
|
20
|
+
* so an agent with no title of its own names its session after its task. Set
|
|
21
|
+
* for Codex, Cursor, Windsurf and Gemini only: Claude Code and opencode title
|
|
22
|
+
* their sessions themselves, and the instruction would cost every turn.
|
|
23
|
+
*/
|
|
24
|
+
exports.AUTONAME_ENV = 'HWIP_AUTONAME';
|
|
18
25
|
/**
|
|
19
26
|
* `mcpServers.hwip` — what the installer writes for Cursor and Windsurf.
|
|
20
27
|
* The env block is deliberate: both spawn the MCP server from a graphical
|
|
@@ -22,12 +29,13 @@ const [MCP_BIN, ...MCP_SUBCOMMAND] = exports.MCP_LAUNCH_ARGV;
|
|
|
22
29
|
* explicitly rather than left to inheritance.
|
|
23
30
|
*
|
|
24
31
|
* `hwipit/plugin`'s `.mcp.json` holds the same shape but is a HAND-KEPT COPY —
|
|
25
|
-
* it lives in another repo and cannot import this. Change one, change both
|
|
32
|
+
* it lives in another repo and cannot import this. Change one, change both —
|
|
33
|
+
* except AUTONAME_ENV, which the plugin (Claude Code) leaves out on purpose.
|
|
26
34
|
*/
|
|
27
35
|
exports.MCP_SERVERS_ENTRY = {
|
|
28
36
|
command: MCP_BIN,
|
|
29
37
|
args: [...MCP_SUBCOMMAND],
|
|
30
|
-
env: { HWIP_API_KEY: '${HWIP_API_KEY}' },
|
|
38
|
+
env: { HWIP_API_KEY: '${HWIP_API_KEY}', [exports.AUTONAME_ENV]: '1' },
|
|
31
39
|
};
|
|
32
40
|
/**
|
|
33
41
|
* opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
|
|
@@ -45,6 +53,11 @@ exports.OPENCODE_MCP_ENTRY = {
|
|
|
45
53
|
// server NAME and the BINARY are both "hwip" here, so the doubled word below
|
|
46
54
|
// is correct, not a copy-paste slip. Default scope is "project" (cwd-local
|
|
47
55
|
// .gemini/), so user scope is pinned for a machine-wide install.
|
|
48
|
-
|
|
56
|
+
// `-e` lands in the entry's env. That a reinstall replaces an existing hwip entry whole, and so
|
|
57
|
+
// adds the flag to it, is unverified (gemini CLI behaviour); `hwip verify` reports it if not.
|
|
58
|
+
exports.GEMINI_MCP_ADD = `gemini mcp add -s user -e ${exports.AUTONAME_ENV}=1 hwip ${MCP_BIN} -- ${MCP_SUBCOMMAND.join(' ')}`;
|
|
49
59
|
/** Older gemini CLIs only accept `mcp add <name> -- <command…>`, with no scope flag. */
|
|
50
|
-
exports.GEMINI_MCP_ADD_LEGACY = `gemini mcp add hwip -- ${exports.MCP_LAUNCH_ARGV.join(' ')}`;
|
|
60
|
+
exports.GEMINI_MCP_ADD_LEGACY = `gemini mcp add -e ${exports.AUTONAME_ENV}=1 hwip -- ${exports.MCP_LAUNCH_ARGV.join(' ')}`;
|
|
61
|
+
/** The legacy form without `-e`, last: should a gemini that old refuse `-e`, the tools still load,
|
|
62
|
+
* and `hwip verify` says auto session names are off. */
|
|
63
|
+
exports.GEMINI_MCP_ADD_BARE = `gemini mcp add hwip -- ${exports.MCP_LAUNCH_ARGV.join(' ')}`;
|
package/dist/pane-fit.d.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
1
2
|
/**
|
|
2
|
-
* Fit a
|
|
3
|
+
* Fit a tmux window to the phone that is watching it.
|
|
3
4
|
*
|
|
4
5
|
* A pane is as wide as the terminal that last drew it, usually a desktop's
|
|
5
6
|
* 120-odd columns. The phone mirror shows about 45, so every full-width row a
|
|
6
7
|
* prompt or TUI draws (a rule, a right-aligned clock, a status line) folds into
|
|
7
|
-
* two.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* two. So while a phone watches a window it is resized to the phone's width
|
|
9
|
+
* and the program redraws to fit; when the phone leaves, the old width comes
|
|
10
|
+
* back.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* attaches
|
|
15
|
-
*
|
|
12
|
+
* A window with a desktop client attached is shared, and whoever used it last
|
|
13
|
+
* decides its width — the rule tmux's own `window-size latest` follows. The
|
|
14
|
+
* phone uses a window when it types into it ({@link claim}); a client uses it
|
|
15
|
+
* when it attaches or a key is pressed in it, which is what
|
|
16
|
+
* `#{client_activity}` records. tmux also moves that time on a mouse move
|
|
17
|
+
* over the window and on a focus change (herdr sends one to a pane whenever
|
|
18
|
+
* another pane is clicked), which are not using it. Where the time of the
|
|
19
|
+
* desktop's last key press is known ({@link macosLastKeyAt}), a client counts
|
|
20
|
+
* as used no later than that key, or than when it attached. Opening the mirror ({@link open}) is a weaker
|
|
21
|
+
* claim: it also covers an automatic resubscribe nobody asked for, so it only
|
|
22
|
+
* takes a window whose desktop has been idle for {@link DESKTOP_IDLE_SECONDS}.
|
|
23
|
+
* While the phone is the later of the two, the
|
|
24
|
+
* window is the phone's width (the desktop terminal shows it narrower); once a
|
|
25
|
+
* client is newer, the next check hands the window back to tmux, which sizes
|
|
26
|
+
* it to that client. A window split into panes is left alone, since the
|
|
27
|
+
* window width is not any one pane's width.
|
|
16
28
|
*
|
|
17
29
|
* A fitted window carries its old width in `@hwip_fit_width`. A listener that
|
|
18
30
|
* died without releasing (a crash, a SIGKILL) leaves that mark behind, and
|
|
@@ -24,6 +36,8 @@ export type TmuxRun = (args: string[]) => {
|
|
|
24
36
|
stdout: string;
|
|
25
37
|
};
|
|
26
38
|
export declare const PANE_FIT_MIN_COLS = 20;
|
|
39
|
+
/** How long a desktop client must sit idle before opening the mirror takes its window. */
|
|
40
|
+
export declare const DESKTOP_IDLE_SECONDS = 60;
|
|
27
41
|
export declare const PANE_FIT_MAX_COLS = 400;
|
|
28
42
|
/** A viewer's column count, or null when it did not send a usable one. */
|
|
29
43
|
export declare const clampViewerCols: (cols: unknown) => number | null;
|
|
@@ -32,9 +46,23 @@ export declare const clampViewerCols: (cols: unknown) => number | null;
|
|
|
32
46
|
* restore its width when detached, then drop the manual size and the mark.
|
|
33
47
|
*/
|
|
34
48
|
export declare const releaseStaleFits: (run: TmuxRun) => number;
|
|
35
|
-
|
|
49
|
+
/** When the desktop behind the tmux client `clientPid` last pressed a key, in seconds since the epoch, or null when it cannot tell. */
|
|
50
|
+
export type LastKeyAt = (clientPid: number) => number | null;
|
|
51
|
+
/**
|
|
52
|
+
* The Mac's last key press, from the HID system's own clock (no Input Monitoring permission needed), read
|
|
53
|
+
* through `osascript` at most once a second and not again for half a minute after a failed read. A client
|
|
54
|
+
* that came in over SSH types on another machine, so the Mac's clock says nothing about it: null, as off macOS.
|
|
55
|
+
*/
|
|
56
|
+
export declare const macosLastKeyAt: (now?: () => number, spawn?: typeof spawnSync, platform?: NodeJS.Platform) => LastKeyAt;
|
|
57
|
+
/** `now` is in ms, as `Date.now`; tmux reports client use in whole seconds. */
|
|
58
|
+
export declare const createPaneFitter: (run: TmuxRun, now?: () => number, lastKeyAt?: LastKeyAt) => {
|
|
36
59
|
fit: (session: string, target: string, cols: unknown) => void;
|
|
60
|
+
recheck: (session: string) => void;
|
|
61
|
+
recheckAll: () => void;
|
|
62
|
+
claim: (session: string) => void;
|
|
63
|
+
open: (session: string) => void;
|
|
37
64
|
release: (session: string) => void;
|
|
38
|
-
fitted: (session: string) => boolean;
|
|
65
|
+
fitted: (session: string, cols?: unknown) => boolean;
|
|
66
|
+
forgetClaims: () => void;
|
|
39
67
|
};
|
|
40
68
|
//# sourceMappingURL=pane-fit.d.ts.map
|