@phnx-labs/agents-cli 1.20.93 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +290 -0
- package/README.md +4 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/feed.js +37 -23
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/monitors.js +8 -6
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +13 -0
- package/dist/lib/activity.js +22 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed-broadcast.d.ts +41 -5
- package/dist/lib/feed-broadcast.js +110 -12
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +3 -1
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/hooks.d.ts +16 -1
- package/dist/lib/hooks.js +61 -1
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +23 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
package/dist/lib/events.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import type { ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
16
|
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
@@ -46,7 +46,7 @@ export interface EventMeta {
|
|
|
46
46
|
/** Resolved actor id — which human/agent is behind this event (RUSH-2020). */
|
|
47
47
|
actor?: string;
|
|
48
48
|
/** Actor kind (`human`/`agent`). */
|
|
49
|
-
kind?: ActorKind;
|
|
49
|
+
kind?: ActorKind | 'unknown';
|
|
50
50
|
}
|
|
51
51
|
export interface EventPayload {
|
|
52
52
|
agent?: string;
|
package/dist/lib/events.js
CHANGED
|
@@ -16,11 +16,9 @@ import * as path from 'path';
|
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
19
|
-
import { parseSshConnection } from './session/provenance.js';
|
|
20
19
|
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
21
20
|
import { getUserAgentsDir } from './state.js';
|
|
22
|
-
import {
|
|
23
|
-
import { machineId } from './machine-id.js';
|
|
21
|
+
import { stampProvenance, resetEventProvenanceForTest } from './event-provenance.js';
|
|
24
22
|
/** Lazy perf warehouse write — avoids a hard cycle at module load. */
|
|
25
23
|
function recordPerfTiming(payload) {
|
|
26
24
|
try {
|
|
@@ -329,51 +327,7 @@ export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.st
|
|
|
329
327
|
}
|
|
330
328
|
return { kind: stdoutIsTTY ? 'terminal' : 'script' };
|
|
331
329
|
}
|
|
332
|
-
|
|
333
|
-
* Who is running this process and from where. Derived once per process from the
|
|
334
|
-
* OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
|
|
335
|
-
* cached — provenance can't change mid-process, so every emit() pays for it once.
|
|
336
|
-
*/
|
|
337
|
-
let _origin;
|
|
338
|
-
function auditOrigin() {
|
|
339
|
-
if (_origin)
|
|
340
|
-
return _origin;
|
|
341
|
-
let osUser = 'unknown';
|
|
342
|
-
try {
|
|
343
|
-
osUser = os.userInfo().username;
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
// Container/edge cases where the uid has no passwd entry.
|
|
347
|
-
}
|
|
348
|
-
const ssh = process.env.SSH_CONNECTION ? parseSshConnection(process.env.SSH_CONNECTION) : undefined;
|
|
349
|
-
const actor = resolveActor();
|
|
350
|
-
_origin = {
|
|
351
|
-
osUser,
|
|
352
|
-
transport: ssh ? 'ssh' : 'local',
|
|
353
|
-
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
354
|
-
actor: actor.id,
|
|
355
|
-
kind: actor.kind,
|
|
356
|
-
};
|
|
357
|
-
return _origin;
|
|
358
|
-
}
|
|
359
|
-
/** This machine's normalized device id, resolved once — it can't change mid-process. */
|
|
360
|
-
let _machineId;
|
|
361
|
-
function cachedMachineId() {
|
|
362
|
-
return (_machineId ??= machineId());
|
|
363
|
-
}
|
|
364
|
-
function resolveProvenance(env = process.env) {
|
|
365
|
-
const p = {};
|
|
366
|
-
const sessionId = env.AGENT_SESSION_ID || env.AGENTS_SESSION_ID;
|
|
367
|
-
if (sessionId)
|
|
368
|
-
p.sessionId = sessionId;
|
|
369
|
-
if (env.AGENTS_AGENT_NAME)
|
|
370
|
-
p.agent = env.AGENTS_AGENT_NAME;
|
|
371
|
-
if (env.AGENT_LAUNCH_ID)
|
|
372
|
-
p.launchId = env.AGENT_LAUNCH_ID;
|
|
373
|
-
if (env.AGENTS_PARENT_SESSION_ID)
|
|
374
|
-
p.parentSessionId = env.AGENTS_PARENT_SESSION_ID;
|
|
375
|
-
return p;
|
|
376
|
-
}
|
|
330
|
+
// ─── Audit attribution ────────────────────────────────────────────────────────
|
|
377
331
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
378
332
|
/**
|
|
379
333
|
* Emit a structured event to the append-only audit log.
|
|
@@ -396,13 +350,12 @@ export function emit(event, payload = {}, overrides = {}) {
|
|
|
396
350
|
const safePayload = sanitizePayload(payload);
|
|
397
351
|
const record = {
|
|
398
352
|
// Provenance floor first: env-sourced defaults an explicit payload overrides.
|
|
399
|
-
...
|
|
353
|
+
...stampProvenance(),
|
|
400
354
|
...safePayload,
|
|
401
355
|
ts: overrides.ts ?? new Date().toISOString(),
|
|
402
356
|
tz: getTimezoneOffset(),
|
|
403
357
|
tzName: getTimezoneName(),
|
|
404
358
|
hostname: os.hostname(),
|
|
405
|
-
machineId: cachedMachineId(),
|
|
406
359
|
platform: os.platform(),
|
|
407
360
|
arch: os.arch(),
|
|
408
361
|
pid: process.pid,
|
|
@@ -411,7 +364,6 @@ export function emit(event, payload = {}, overrides = {}) {
|
|
|
411
364
|
level: levelFor(event),
|
|
412
365
|
caller: caller.kind,
|
|
413
366
|
...(caller.session ? { session: caller.session } : {}),
|
|
414
|
-
...auditOrigin(),
|
|
415
367
|
};
|
|
416
368
|
const line = JSON.stringify(record) + '\n';
|
|
417
369
|
const logPath = eventsPath();
|
|
@@ -919,8 +871,7 @@ export function getLogsPath() {
|
|
|
919
871
|
}
|
|
920
872
|
export function _resetForTest(overrideEventsPath) {
|
|
921
873
|
_eventsPath = overrideEventsPath;
|
|
922
|
-
|
|
923
|
-
_machineId = undefined;
|
|
874
|
+
resetEventProvenanceForTest();
|
|
924
875
|
_chmoddedPath = undefined;
|
|
925
876
|
lastRotationCheck = 0;
|
|
926
877
|
}
|
|
@@ -16,7 +16,9 @@ export interface FeedSinkConfig {
|
|
|
16
16
|
export type FeedBroadcastConfig = Record<string, FeedSinkConfig>;
|
|
17
17
|
/** Everything a template may interpolate. Absent values skip templates that need them. */
|
|
18
18
|
export interface FeedBroadcastContext {
|
|
19
|
-
/**
|
|
19
|
+
/** Short subject line (~4–5 words). Phone line 1. */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** The post body, verbatim. Phone line after the blank line. */
|
|
20
22
|
text: string;
|
|
21
23
|
level: FeedPostLevel;
|
|
22
24
|
/** Tracker id for the work, e.g. `RUSH-2081`. */
|
|
@@ -63,6 +65,8 @@ export declare function blockBroadcastContext(block: {
|
|
|
63
65
|
}, extras?: {
|
|
64
66
|
project?: string;
|
|
65
67
|
agent?: string;
|
|
68
|
+
title?: string;
|
|
69
|
+
body?: string;
|
|
66
70
|
}): FeedBroadcastContext;
|
|
67
71
|
/**
|
|
68
72
|
* Why a declared block reached nobody, or undefined when it got through.
|
|
@@ -87,10 +91,42 @@ export interface SinkOutcome {
|
|
|
87
91
|
error?: string;
|
|
88
92
|
}
|
|
89
93
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
* Short host label for a phone line — strip user@ and domain so
|
|
95
|
+
* `muqsit@mac-mini.tailnet.ts.net` reads as `mac-mini`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function shortHost(host: string | undefined): string | undefined;
|
|
98
|
+
/** First 8 hex chars of a session id for the footer (readable, not a full uuid). */
|
|
99
|
+
export declare function shortSessionChunk(session: string | undefined): string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Scrub em/en dashes from outbound phone copy (house rule + iMessage readability).
|
|
102
|
+
* Collapses whitespace; does not invent meaning.
|
|
103
|
+
*/
|
|
104
|
+
export declare function scrubOutboundDashes(text: string): string;
|
|
105
|
+
/**
|
|
106
|
+
* Footer like "Sent from my iPhone" — who posted, a session crumb, which box.
|
|
107
|
+
*
|
|
108
|
+
* Sent from grok/a02da0e2 on mac-mini
|
|
109
|
+
*
|
|
110
|
+
* Agent name first; session chunk for disambiguation when many groks run;
|
|
111
|
+
* host last. Skip the uninformative default label `agent`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): string | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Human-facing body for a messaging sink (`{message}`).
|
|
116
|
+
*
|
|
117
|
+
* ```
|
|
118
|
+
* Title in a few words
|
|
119
|
+
*
|
|
120
|
+
* Body of what happened or the ask.
|
|
121
|
+
*
|
|
122
|
+
* Sent from grok/a02da0e2 on mac-mini
|
|
123
|
+
* agents focus a02da0e2 (blocks only)
|
|
124
|
+
* https://… (optional attach URL)
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* Title first (scannable subject). Blank line. Body. Footer provenance so a
|
|
128
|
+
* fleet of agents is attributable without crowding the ask. Prefer `{message}`
|
|
129
|
+
* over bare `{text}` in messaging sinks.
|
|
94
130
|
*/
|
|
95
131
|
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
|
|
96
132
|
/**
|
|
@@ -50,8 +50,12 @@ export function parseFeedPostLevel(raw) {
|
|
|
50
50
|
export function blockBroadcastContext(block, extras = {}) {
|
|
51
51
|
const ask = block.questions?.[0]?.text?.trim() || 'agent is blocked';
|
|
52
52
|
const links = [block.pr].filter((l) => !!l && /^https?:\/\//i.test(l));
|
|
53
|
+
// Prefer explicit title/body from the feed post; fall back to the ask as body.
|
|
54
|
+
const title = extras.title?.trim() || undefined;
|
|
55
|
+
const text = extras.body?.trim() || ask;
|
|
53
56
|
return {
|
|
54
|
-
|
|
57
|
+
...(title ? { title } : {}),
|
|
58
|
+
text,
|
|
55
59
|
level: 'important',
|
|
56
60
|
ticket: block.ticket,
|
|
57
61
|
project: extras.project,
|
|
@@ -92,24 +96,118 @@ export function blockDeliveryFailure(blocked, outcomes) {
|
|
|
92
96
|
}
|
|
93
97
|
const PLACEHOLDER = /\{([a-z]+)\}/g;
|
|
94
98
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
* Short host label for a phone line — strip user@ and domain so
|
|
100
|
+
* `muqsit@mac-mini.tailnet.ts.net` reads as `mac-mini`.
|
|
101
|
+
*/
|
|
102
|
+
export function shortHost(host) {
|
|
103
|
+
if (!host?.trim())
|
|
104
|
+
return undefined;
|
|
105
|
+
let h = host.trim();
|
|
106
|
+
const at = h.lastIndexOf('@');
|
|
107
|
+
if (at !== -1)
|
|
108
|
+
h = h.slice(at + 1);
|
|
109
|
+
const dot = h.indexOf('.');
|
|
110
|
+
if (dot > 0)
|
|
111
|
+
h = h.slice(0, dot);
|
|
112
|
+
return h || undefined;
|
|
113
|
+
}
|
|
114
|
+
/** First 8 hex chars of a session id for the footer (readable, not a full uuid). */
|
|
115
|
+
export function shortSessionChunk(session) {
|
|
116
|
+
if (!session?.trim())
|
|
117
|
+
return undefined;
|
|
118
|
+
const hex = session.replace(/-/g, '').toLowerCase();
|
|
119
|
+
const chunk = hex.replace(/[^a-f0-9]/g, '').slice(0, 8);
|
|
120
|
+
return chunk || undefined;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Scrub em/en dashes from outbound phone copy (house rule + iMessage readability).
|
|
124
|
+
* Collapses whitespace; does not invent meaning.
|
|
125
|
+
*/
|
|
126
|
+
export function scrubOutboundDashes(text) {
|
|
127
|
+
return text
|
|
128
|
+
.replace(/\u2014/g, ' - ')
|
|
129
|
+
.replace(/\u2013/g, ' - ')
|
|
130
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
131
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
132
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
133
|
+
.trim();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Footer like "Sent from my iPhone" — who posted, a session crumb, which box.
|
|
137
|
+
*
|
|
138
|
+
* Sent from grok/a02da0e2 on mac-mini
|
|
139
|
+
*
|
|
140
|
+
* Agent name first; session chunk for disambiguation when many groks run;
|
|
141
|
+
* host last. Skip the uninformative default label `agent`.
|
|
142
|
+
*/
|
|
143
|
+
export function composeBroadcastFooter(ctx) {
|
|
144
|
+
const agent = ctx.agent?.trim();
|
|
145
|
+
const agentLabel = agent && agent !== 'agent' ? agent : undefined;
|
|
146
|
+
const session = shortSessionChunk(ctx.session);
|
|
147
|
+
const host = shortHost(ctx.host);
|
|
148
|
+
let who;
|
|
149
|
+
if (agentLabel && session)
|
|
150
|
+
who = `${agentLabel}/${session}`;
|
|
151
|
+
else if (agentLabel)
|
|
152
|
+
who = agentLabel;
|
|
153
|
+
else if (session)
|
|
154
|
+
who = session;
|
|
155
|
+
if (who && host)
|
|
156
|
+
return `Sent from ${who} on ${host}`;
|
|
157
|
+
if (who)
|
|
158
|
+
return `Sent from ${who}`;
|
|
159
|
+
if (host)
|
|
160
|
+
return `Sent from host ${host}`;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Human-facing body for a messaging sink (`{message}`).
|
|
165
|
+
*
|
|
166
|
+
* ```
|
|
167
|
+
* Title in a few words
|
|
168
|
+
*
|
|
169
|
+
* Body of what happened or the ask.
|
|
170
|
+
*
|
|
171
|
+
* Sent from grok/a02da0e2 on mac-mini
|
|
172
|
+
* agents focus a02da0e2 (blocks only)
|
|
173
|
+
* https://… (optional attach URL)
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* Title first (scannable subject). Blank line. Body. Footer provenance so a
|
|
177
|
+
* fleet of agents is attributable without crowding the ask. Prefer `{message}`
|
|
178
|
+
* over bare `{text}` in messaging sinks.
|
|
99
179
|
*/
|
|
100
180
|
export function composeBroadcastMessage(ctx) {
|
|
101
|
-
const
|
|
181
|
+
const title = scrubOutboundDashes(ctx.title ?? '');
|
|
182
|
+
const body = scrubOutboundDashes(ctx.text ?? '');
|
|
183
|
+
// Title preferred; if an older post has no title, body alone still sends.
|
|
184
|
+
const head = title || body;
|
|
185
|
+
const mid = title && body && title !== body ? body : undefined;
|
|
186
|
+
const footer = composeBroadcastFooter(ctx);
|
|
102
187
|
const link = ctx.links?.find((l) => /^https?:\/\//i.test(l));
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
188
|
+
// Block focus and link trail after the "Sent from" footer so the human
|
|
189
|
+
// sentence stays at the top and the action/link are still one glance away.
|
|
190
|
+
const trail = [footer, ctx.focus, link].filter(Boolean);
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (head)
|
|
193
|
+
parts.push(head);
|
|
194
|
+
if (mid) {
|
|
195
|
+
// Blank line between subject and body (title, then space, then message).
|
|
196
|
+
parts.push('');
|
|
197
|
+
parts.push(mid);
|
|
198
|
+
}
|
|
199
|
+
if (trail.length) {
|
|
200
|
+
// Blank line before the footer block (iPhone "Sent from my iPhone" spacing).
|
|
201
|
+
if (parts.length)
|
|
202
|
+
parts.push('');
|
|
203
|
+
parts.push(trail.join('\n'));
|
|
204
|
+
}
|
|
205
|
+
return parts.join('\n').trim();
|
|
109
206
|
}
|
|
110
207
|
/** The values a template may reference, resolved once per post. */
|
|
111
208
|
function templateVars(ctx) {
|
|
112
209
|
return {
|
|
210
|
+
title: ctx.title,
|
|
113
211
|
text: ctx.text,
|
|
114
212
|
ticket: ctx.ticket,
|
|
115
213
|
project: ctx.project,
|
package/dist/lib/feed-post.d.ts
CHANGED
|
@@ -2,8 +2,15 @@ import { type ActivityEvent, type Attachment } from './activity.js';
|
|
|
2
2
|
import { type PidSessionEntry } from './session/pid-registry.js';
|
|
3
3
|
/** Soft cap so a runaway agent can't flood the activity lane with essays. */
|
|
4
4
|
export declare const STATUS_POST_MAX_CHARS = 500;
|
|
5
|
+
/** Title is a phone subject line - about four or five words, not a paragraph. */
|
|
6
|
+
export declare const STATUS_TITLE_MAX_CHARS = 60;
|
|
5
7
|
export interface FeedPostInput {
|
|
6
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Short subject (required for new posts). ~4–5 words. Phone broadcasts put
|
|
10
|
+
* this on the first line so a scan names the topic before the body.
|
|
11
|
+
*/
|
|
12
|
+
title: string;
|
|
13
|
+
/** Body text (required). Domain-agnostic free text — what happened / the ask. */
|
|
7
14
|
text: string;
|
|
8
15
|
/** Override session id (escape hatch for scripts/tests). Prefer auto-resolve. */
|
|
9
16
|
sessionId?: string;
|
|
@@ -92,9 +99,16 @@ export declare function buildAttachments(tokens: string[] | undefined, ctx: {
|
|
|
92
99
|
sessionId: string;
|
|
93
100
|
updateId: string;
|
|
94
101
|
}): Attachment[];
|
|
102
|
+
/**
|
|
103
|
+
* Collapse whitespace and strip em/en dashes (house rule: no em-dashes in
|
|
104
|
+
* agent-authored outbound copy — phones and plain text render them poorly).
|
|
105
|
+
*/
|
|
106
|
+
export declare function scrubDashes(text: string): string;
|
|
95
107
|
export declare function normalizeStatusText(text: string): string;
|
|
108
|
+
/** Normalize a short subject line for a post. */
|
|
109
|
+
export declare function normalizeStatusTitle(title: string): string;
|
|
96
110
|
/**
|
|
97
111
|
* Append a `status.posted` milestone for the calling agent.
|
|
98
|
-
* Throws if text is empty or session identity cannot be resolved.
|
|
112
|
+
* Throws if title/text is empty or session identity cannot be resolved.
|
|
99
113
|
*/
|
|
100
114
|
export declare function postFeedStatus(input: FeedPostInput): FeedPostResult;
|
package/dist/lib/feed-post.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent status posts — deliberate progress messages into the activity stream.
|
|
3
3
|
*
|
|
4
|
-
* Surface: `agents feed post <
|
|
5
|
-
* `agents feed` / `agents activity` / `agents events --module activity`).
|
|
4
|
+
* Surface: `agents feed post --title <subject> <body>` (agent-callable; humans
|
|
5
|
+
* watch via `agents feed` / `agents activity` / `agents events --module activity`).
|
|
6
6
|
*
|
|
7
7
|
* Identity is automatic: session id, agent, cwd, launch/pid/tmux provenance
|
|
8
8
|
* are resolved from the process environment and the per-pid launch registry
|
|
9
|
-
* (`lib/session/pid-registry.ts`). The agent
|
|
10
|
-
* no domain-specific flags (tickets, URLs, tracks).
|
|
9
|
+
* (`lib/session/pid-registry.ts`). The agent authors a short title + body —
|
|
10
|
+
* no domain-specific flags (tickets, URLs, tracks). Phone `{message}` ends with
|
|
11
|
+
* a "Sent from agent/session on host" footer.
|
|
11
12
|
*
|
|
12
13
|
* Storage: append-only activity log as a `status.posted` milestone. Does NOT
|
|
13
14
|
* open a feed block (blocks remain "needs you" state only).
|
|
@@ -23,6 +24,8 @@ import { isValidMailboxId } from './mailbox.js';
|
|
|
23
24
|
import { listPidSessionEntries, readPidSessionEntry, } from './session/pid-registry.js';
|
|
24
25
|
/** Soft cap so a runaway agent can't flood the activity lane with essays. */
|
|
25
26
|
export const STATUS_POST_MAX_CHARS = 500;
|
|
27
|
+
/** Title is a phone subject line - about four or five words, not a paragraph. */
|
|
28
|
+
export const STATUS_TITLE_MAX_CHARS = 60;
|
|
26
29
|
/**
|
|
27
30
|
* Resolve who is posting. Order:
|
|
28
31
|
* 1. Explicit --session flag
|
|
@@ -241,22 +244,46 @@ export function buildAttachments(tokens, ctx) {
|
|
|
241
244
|
}
|
|
242
245
|
return out;
|
|
243
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Collapse whitespace and strip em/en dashes (house rule: no em-dashes in
|
|
249
|
+
* agent-authored outbound copy — phones and plain text render them poorly).
|
|
250
|
+
*/
|
|
251
|
+
export function scrubDashes(text) {
|
|
252
|
+
return text
|
|
253
|
+
.replace(/\u2014/g, ' - ') // em dash —
|
|
254
|
+
.replace(/\u2013/g, ' - ') // en dash –
|
|
255
|
+
.replace(/\s+/g, ' ')
|
|
256
|
+
.trim();
|
|
257
|
+
}
|
|
244
258
|
export function normalizeStatusText(text) {
|
|
245
|
-
const collapsed = text
|
|
259
|
+
const collapsed = scrubDashes(text);
|
|
246
260
|
if (!collapsed)
|
|
247
261
|
return '';
|
|
248
262
|
if (collapsed.length <= STATUS_POST_MAX_CHARS)
|
|
249
263
|
return collapsed;
|
|
250
264
|
return `${collapsed.slice(0, STATUS_POST_MAX_CHARS - 1)}…`;
|
|
251
265
|
}
|
|
266
|
+
/** Normalize a short subject line for a post. */
|
|
267
|
+
export function normalizeStatusTitle(title) {
|
|
268
|
+
const collapsed = scrubDashes(title);
|
|
269
|
+
if (!collapsed)
|
|
270
|
+
return '';
|
|
271
|
+
if (collapsed.length <= STATUS_TITLE_MAX_CHARS)
|
|
272
|
+
return collapsed;
|
|
273
|
+
return `${collapsed.slice(0, STATUS_TITLE_MAX_CHARS - 1)}…`;
|
|
274
|
+
}
|
|
252
275
|
/**
|
|
253
276
|
* Append a `status.posted` milestone for the calling agent.
|
|
254
|
-
* Throws if text is empty or session identity cannot be resolved.
|
|
277
|
+
* Throws if title/text is empty or session identity cannot be resolved.
|
|
255
278
|
*/
|
|
256
279
|
export function postFeedStatus(input) {
|
|
280
|
+
const title = normalizeStatusTitle(input.title ?? '');
|
|
257
281
|
const detail = normalizeStatusText(input.text);
|
|
282
|
+
if (!title) {
|
|
283
|
+
throw new Error('Title is empty. Usage: agents feed post --title "Short subject" "what just happened"');
|
|
284
|
+
}
|
|
258
285
|
if (!detail) {
|
|
259
|
-
throw new Error('Status text is empty. Usage: agents feed post "what just happened"');
|
|
286
|
+
throw new Error('Status text is empty. Usage: agents feed post --title "Short subject" "what just happened"');
|
|
260
287
|
}
|
|
261
288
|
const identity = resolvePostIdentity(input);
|
|
262
289
|
if (!identity) {
|
|
@@ -285,6 +312,7 @@ export function postFeedStatus(input) {
|
|
|
285
312
|
cwd: identity.cwd,
|
|
286
313
|
agent: identity.agent,
|
|
287
314
|
tool: 'feed.post',
|
|
315
|
+
title,
|
|
288
316
|
detail,
|
|
289
317
|
...(project ? { project } : {}),
|
|
290
318
|
...(identity.pid !== undefined ? { pid: identity.pid } : {}),
|
package/dist/lib/feed.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface MessageReceipt {
|
|
|
12
12
|
/** The message id this receipt describes. */
|
|
13
13
|
msgId: string;
|
|
14
14
|
/** Delivery lifecycle state. */
|
|
15
|
-
status: 'queued' | 'consumed' | 'continued';
|
|
15
|
+
status: 'queued' | 'consumed' | 'continued' | 'dropped' | 'expired';
|
|
16
16
|
/** ISO-8601 timestamp of the state transition. */
|
|
17
17
|
at: string;
|
|
18
18
|
/** Optional sender label for the message. */
|
package/dist/lib/feed.js
CHANGED
|
@@ -153,6 +153,8 @@ const RECEIPT_STATUS_RANK = {
|
|
|
153
153
|
queued: 0,
|
|
154
154
|
consumed: 1,
|
|
155
155
|
continued: 2,
|
|
156
|
+
dropped: 3,
|
|
157
|
+
expired: 3,
|
|
156
158
|
};
|
|
157
159
|
/**
|
|
158
160
|
* Record a delivery-receipt transition for a message tied to a block.
|
|
@@ -253,7 +255,7 @@ export function clearBlockLifecycle(blockId, root) {
|
|
|
253
255
|
export function buildDeclaredBlock(agent, input) {
|
|
254
256
|
const text = input.text.trim().replace(/\s+/g, ' ');
|
|
255
257
|
if (!text) {
|
|
256
|
-
throw new Error('Block text is empty. Usage: agents feed post "what you need from the user" --blocked');
|
|
258
|
+
throw new Error('Block text is empty. Usage: agents feed post --title "Short subject" "what you need from the user" --blocked');
|
|
257
259
|
}
|
|
258
260
|
const options = (input.options ?? [])
|
|
259
261
|
.map((label) => label.trim())
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -41,8 +41,12 @@ export declare function assertValidBranchName(branch: string): void;
|
|
|
41
41
|
*
|
|
42
42
|
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
43
43
|
* repo state rather than a hard-coded literal.
|
|
44
|
+
*
|
|
45
|
+
* Pass `targetBranch` to push the local `branch` to a differently-named remote
|
|
46
|
+
* branch (`git push origin <branch>:<targetBranch>`) — used when publishing the
|
|
47
|
+
* working tree to a branch other than the checked-out one.
|
|
44
48
|
*/
|
|
45
|
-
export declare function pushOrigin(git: SimpleGit, branch: string): Promise<void>;
|
|
49
|
+
export declare function pushOrigin(git: SimpleGit, branch: string, targetBranch?: string): Promise<void>;
|
|
46
50
|
/** Parsed representation of a git source string (GitHub, generic URL, or local path). */
|
|
47
51
|
export interface GitSource {
|
|
48
52
|
type: 'github' | 'url' | 'local';
|
|
@@ -124,6 +128,8 @@ export declare function resolveGitHubUsername(): Promise<string | null>;
|
|
|
124
128
|
* Get the remote URL for origin in a git repo.
|
|
125
129
|
*/
|
|
126
130
|
export declare function getRemoteUrl(repoPath: string): Promise<string | null>;
|
|
131
|
+
/** The repo's checked-out branch, or 'main' on a detached HEAD / read failure. */
|
|
132
|
+
export declare function getCurrentBranch(repoPath: string): Promise<string>;
|
|
127
133
|
/**
|
|
128
134
|
* Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
|
|
129
135
|
* same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
|
|
@@ -158,8 +164,13 @@ export type CommitAndPushResult = {
|
|
|
158
164
|
* Clean tree + local ahead of origin still pushes — "nothing to commit" is not
|
|
159
165
|
* "nothing to push". Reports "already up to date" only when `ahead === 0` and
|
|
160
166
|
* there is nothing to commit.
|
|
167
|
+
*
|
|
168
|
+
* `targetBranch` pushes the working tree to a differently-named remote branch
|
|
169
|
+
* (`<current>:<targetBranch>`) and is reported back as the result `branch`, so
|
|
170
|
+
* callers that print a branch-scoped URL reference where the commit actually
|
|
171
|
+
* landed — not the checked-out branch.
|
|
161
172
|
*/
|
|
162
|
-
export declare function commitAndPush(repoPath: string, message: string): Promise<CommitAndPushResult>;
|
|
173
|
+
export declare function commitAndPush(repoPath: string, message: string, targetBranch?: string): Promise<CommitAndPushResult>;
|
|
163
174
|
/**
|
|
164
175
|
* Check if repo has uncommitted changes.
|
|
165
176
|
*/
|
package/dist/lib/git.js
CHANGED
|
@@ -77,9 +77,18 @@ export function assertValidBranchName(branch) {
|
|
|
77
77
|
*
|
|
78
78
|
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
79
79
|
* repo state rather than a hard-coded literal.
|
|
80
|
+
*
|
|
81
|
+
* Pass `targetBranch` to push the local `branch` to a differently-named remote
|
|
82
|
+
* branch (`git push origin <branch>:<targetBranch>`) — used when publishing the
|
|
83
|
+
* working tree to a branch other than the checked-out one.
|
|
80
84
|
*/
|
|
81
|
-
export async function pushOrigin(git, branch) {
|
|
85
|
+
export async function pushOrigin(git, branch, targetBranch) {
|
|
82
86
|
assertValidBranchName(branch);
|
|
87
|
+
if (targetBranch && targetBranch !== branch) {
|
|
88
|
+
assertValidBranchName(targetBranch);
|
|
89
|
+
await git.raw(['push', '--', 'origin', `${branch}:${targetBranch}`]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
83
92
|
await git.raw(['push', '--', 'origin', branch]);
|
|
84
93
|
}
|
|
85
94
|
/**
|
|
@@ -435,6 +444,16 @@ export async function getRemoteUrl(repoPath) {
|
|
|
435
444
|
return null;
|
|
436
445
|
}
|
|
437
446
|
}
|
|
447
|
+
/** The repo's checked-out branch, or 'main' on a detached HEAD / read failure. */
|
|
448
|
+
export async function getCurrentBranch(repoPath) {
|
|
449
|
+
try {
|
|
450
|
+
const status = await simpleGit(repoPath).status();
|
|
451
|
+
return status.current || 'main';
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return 'main';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
438
457
|
/**
|
|
439
458
|
* Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
|
|
440
459
|
* same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
|
|
@@ -494,13 +513,23 @@ export async function checkGitHubRepoExists(owner, repo) {
|
|
|
494
513
|
* Clean tree + local ahead of origin still pushes — "nothing to commit" is not
|
|
495
514
|
* "nothing to push". Reports "already up to date" only when `ahead === 0` and
|
|
496
515
|
* there is nothing to commit.
|
|
516
|
+
*
|
|
517
|
+
* `targetBranch` pushes the working tree to a differently-named remote branch
|
|
518
|
+
* (`<current>:<targetBranch>`) and is reported back as the result `branch`, so
|
|
519
|
+
* callers that print a branch-scoped URL reference where the commit actually
|
|
520
|
+
* landed — not the checked-out branch.
|
|
497
521
|
*/
|
|
498
|
-
export async function commitAndPush(repoPath, message) {
|
|
522
|
+
export async function commitAndPush(repoPath, message, targetBranch) {
|
|
499
523
|
try {
|
|
500
524
|
const git = simpleGit(repoPath);
|
|
501
525
|
let status = await git.status();
|
|
502
526
|
const branch = status.current || 'main';
|
|
503
527
|
assertValidBranchName(branch);
|
|
528
|
+
if (targetBranch)
|
|
529
|
+
assertValidBranchName(targetBranch);
|
|
530
|
+
// The branch the commit ends up on remotely — the checked-out branch unless
|
|
531
|
+
// an explicit target was requested.
|
|
532
|
+
const pushedBranch = targetBranch || branch;
|
|
504
533
|
let committed = false;
|
|
505
534
|
if (status.files.length > 0) {
|
|
506
535
|
await git.add('-A');
|
|
@@ -509,7 +538,10 @@ export async function commitAndPush(repoPath, message) {
|
|
|
509
538
|
status = await git.status();
|
|
510
539
|
}
|
|
511
540
|
const ahead = status.ahead ?? 0;
|
|
512
|
-
|
|
541
|
+
// A same-branch push short-circuits when there is nothing new; a push to a
|
|
542
|
+
// different target branch must still run even from a clean, non-ahead tree,
|
|
543
|
+
// since the target may not carry these commits yet.
|
|
544
|
+
if (!committed && ahead === 0 && pushedBranch === branch) {
|
|
513
545
|
return {
|
|
514
546
|
success: true,
|
|
515
547
|
detail: 'already up to date',
|
|
@@ -521,12 +553,12 @@ export async function commitAndPush(repoPath, message) {
|
|
|
521
553
|
// Capture remote tip before push for a real ref range in the detail string.
|
|
522
554
|
let before = '';
|
|
523
555
|
try {
|
|
524
|
-
before = (await git.raw(['rev-parse', '--short=8', `origin/${
|
|
556
|
+
before = (await git.raw(['rev-parse', '--short=8', `origin/${pushedBranch}`])).trim();
|
|
525
557
|
}
|
|
526
558
|
catch {
|
|
527
559
|
/* origin/<branch> may not exist yet (first push) */
|
|
528
560
|
}
|
|
529
|
-
await pushOrigin(git, branch);
|
|
561
|
+
await pushOrigin(git, branch, targetBranch);
|
|
530
562
|
let after = '';
|
|
531
563
|
try {
|
|
532
564
|
after = (await git.raw(['rev-parse', '--short=8', 'HEAD'])).trim();
|
|
@@ -547,7 +579,7 @@ export async function commitAndPush(repoPath, message) {
|
|
|
547
579
|
return {
|
|
548
580
|
success: true,
|
|
549
581
|
detail,
|
|
550
|
-
branch,
|
|
582
|
+
branch: pushedBranch,
|
|
551
583
|
committed,
|
|
552
584
|
pushed: true,
|
|
553
585
|
};
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -198,6 +198,19 @@ export declare function installHooksCentrally(source: string): Promise<{
|
|
|
198
198
|
* User dir takes priority; deduplication preserves first occurrence.
|
|
199
199
|
*/
|
|
200
200
|
export declare function listCentralHooks(): HookEntry[];
|
|
201
|
+
/**
|
|
202
|
+
* Normalize a hook `timeout` from agents.yaml into a whole number of seconds.
|
|
203
|
+
*
|
|
204
|
+
* A bare number stays seconds (`timeout: 30` → 30) for backward compatibility.
|
|
205
|
+
* A Go-style duration string is parsed into seconds: `5s`, `2m`, `1h30m`,
|
|
206
|
+
* `90s`, `1h`. This intentionally does NOT reuse {@link parseTimeout} from
|
|
207
|
+
* routines.ts — that one returns milliseconds, has no seconds (`s`) unit, and
|
|
208
|
+
* floors at one minute, none of which fit hook timeouts (typically 5–600s).
|
|
209
|
+
*
|
|
210
|
+
* Returns the seconds value, or `null` when the input is not a positive number
|
|
211
|
+
* or a parseable duration string — the caller decides how to surface that.
|
|
212
|
+
*/
|
|
213
|
+
export declare function normalizeHookTimeoutSeconds(value: unknown): number | null;
|
|
201
214
|
/**
|
|
202
215
|
* Parse hook manifests. Reads system hooks from ~/.agents/.system/hooks.yaml
|
|
203
216
|
* (npm-shipped defaults) and user hooks from the `hooks:` section of
|
|
@@ -205,7 +218,9 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
205
218
|
* A user entry with `enabled: false` disables the system-shipped hook of
|
|
206
219
|
* the same name without forking the system file.
|
|
207
220
|
*
|
|
208
|
-
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
221
|
+
* Hooks marked `enabled: false` are dropped from the returned map. A hook
|
|
222
|
+
* `timeout` written as a duration string (`5s`, `2m`) is normalized to a
|
|
223
|
+
* seconds number here, so every downstream serializer keeps reading a number.
|
|
209
224
|
*/
|
|
210
225
|
export declare function parseHookManifest(opts?: {
|
|
211
226
|
warn?: boolean;
|