@4pm/cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/index.js +347 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# @4pm/cli
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
3
|
+
**An agent running on the worker machine** — receives commands from `@4pm/server` over WebSocket and orchestrates
|
|
4
|
+
the AI CLIs (`claude` / `codex`) plus other tools (`git`, `gh`, `glab`, `shell`).
|
|
5
5
|
Node ESM + TypeScript.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
WebSocket
|
|
9
|
-
server.
|
|
7
|
+
Lifecycle: **pairing** with the server via a hashcode (the `.cre` file) → request a `ws_token` → open a
|
|
8
|
+
TLS WebSocket → receive commands → **spawn child processes**, gather the output (backpressure) back to the
|
|
9
|
+
server. Multiple instances on one machine use separate profiles, and the server groups them into one **worker**.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
pnpm --filter @4pm/cli dev
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
>
|
|
16
|
-
>
|
|
15
|
+
> Code organization, conventions & what's missing: [`CLAUDE.md`](./CLAUDE.md).
|
|
16
|
+
> The cli behavior spec: [`11-docs/61-cli/`](../../11-docs/61-cli/README.md).
|
package/dist/index.js
CHANGED
|
@@ -24469,6 +24469,27 @@ function profileDir(name) {
|
|
|
24469
24469
|
mkdirSync(dir2, { recursive: true });
|
|
24470
24470
|
return dir2;
|
|
24471
24471
|
}
|
|
24472
|
+
function defaultProfileConfig() {
|
|
24473
|
+
return {
|
|
24474
|
+
aiCli: "claude",
|
|
24475
|
+
// Lists so failover across profiles is discoverable (ADR-0057) — add more objects.
|
|
24476
|
+
// `args` are the full per-CLI defaults (claude stream-json for metering — ADR-0072).
|
|
24477
|
+
// `model` = the CLI's `--model` flag; default "sonnet" — the balanced mid-price tier
|
|
24478
|
+
// (cheaper than opus, more capable than haiku). "" ⇒ the CLI's own default model.
|
|
24479
|
+
claudeHome: [
|
|
24480
|
+
{ profile: ".claude", args: ["-p", "--output-format", "stream-json", "--verbose"], model: "sonnet" }
|
|
24481
|
+
],
|
|
24482
|
+
codexHome: [{ profile: ".codex", args: ["exec"], model: "" }],
|
|
24483
|
+
antigravityHome: [],
|
|
24484
|
+
autoUpdate: true,
|
|
24485
|
+
commandHistoryUploadMinutes: 10
|
|
24486
|
+
};
|
|
24487
|
+
}
|
|
24488
|
+
function ensureProfileConfig(dir2) {
|
|
24489
|
+
if (existsSync(join(dir2, "config.json"))) return false;
|
|
24490
|
+
writeProfileConfig(dir2, defaultProfileConfig());
|
|
24491
|
+
return true;
|
|
24492
|
+
}
|
|
24472
24493
|
function readProfileConfig(dir2) {
|
|
24473
24494
|
const path = join(dir2, "config.json");
|
|
24474
24495
|
if (!existsSync(path)) return {};
|
|
@@ -24483,6 +24504,10 @@ function writeProfileConfig(dir2, patch) {
|
|
|
24483
24504
|
writeFileSync(join(dir2, "config.json"), JSON.stringify(merged, null, 2), "utf8");
|
|
24484
24505
|
return merged;
|
|
24485
24506
|
}
|
|
24507
|
+
function overwriteProfileConfig(dir2, config) {
|
|
24508
|
+
writeFileSync(join(dir2, "config.json"), JSON.stringify(config, null, 2), "utf8");
|
|
24509
|
+
return config;
|
|
24510
|
+
}
|
|
24486
24511
|
function deleteProfileConfigKey(dir2, key) {
|
|
24487
24512
|
const config = readProfileConfig(dir2);
|
|
24488
24513
|
delete config[key];
|
|
@@ -24638,15 +24663,17 @@ async function runLink(serverUrl, explicitProfile) {
|
|
|
24638
24663
|
);
|
|
24639
24664
|
const profileName = explicitProfile ?? sanitizeProfileName(username);
|
|
24640
24665
|
const existed = readCredential(profileDir(profileName), profileName) != null;
|
|
24641
|
-
|
|
24666
|
+
const dir2 = profileDir(profileName);
|
|
24667
|
+
writeCredential(dir2, {
|
|
24642
24668
|
serverUrl,
|
|
24643
24669
|
hashcode3,
|
|
24644
24670
|
scope,
|
|
24645
24671
|
// decided by the server from the pairing user (ADR-0010); shown in the TUI
|
|
24646
24672
|
pairedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
24647
24673
|
});
|
|
24674
|
+
ensureProfileConfig(dir2);
|
|
24648
24675
|
if (projectName && scope === "project") {
|
|
24649
|
-
const folder = join3(
|
|
24676
|
+
const folder = join3(dir2, projectName);
|
|
24650
24677
|
mkdirSync2(folder, { recursive: true });
|
|
24651
24678
|
console.log(`\u2714 Created physic project folder: ${folder}`);
|
|
24652
24679
|
}
|
|
@@ -24963,6 +24990,9 @@ var PermissionCode = {
|
|
|
24963
24990
|
// billing
|
|
24964
24991
|
BILLING_READ: "billing.read",
|
|
24965
24992
|
BILLING_MANAGE: "billing.manage",
|
|
24993
|
+
// org template files (ADR-0113) — ADMIN + PM manage the org's template library
|
|
24994
|
+
TEMPLATE_READ: "template.read",
|
|
24995
|
+
TEMPLATE_WRITE: "template.write",
|
|
24966
24996
|
// integration (community-server — ADR-0058; served by @4pm/community)
|
|
24967
24997
|
INTEGRATION_READ: "integration.read",
|
|
24968
24998
|
INTEGRATION_CONNECT: "integration.connect",
|
|
@@ -25006,6 +25036,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
25006
25036
|
P.USER_INVITE,
|
|
25007
25037
|
P.QUOTA_READ,
|
|
25008
25038
|
P.QUOTA_MANAGE,
|
|
25039
|
+
P.TEMPLATE_READ,
|
|
25040
|
+
P.TEMPLATE_WRITE,
|
|
25009
25041
|
P.TEAM_LIST,
|
|
25010
25042
|
P.TEAM_CREATE,
|
|
25011
25043
|
P.TEAM_READ,
|
|
@@ -29351,16 +29383,30 @@ var resetPasswordRequestSchema = external_exports.object({
|
|
|
29351
29383
|
|
|
29352
29384
|
// ../../22-packages/11-shared/31-dto/src/org.ts
|
|
29353
29385
|
var CLI_SETTINGS_BOUNDS = {
|
|
29354
|
-
reconnectMaxBackoffSec: { min: 5, max:
|
|
29386
|
+
reconnectMaxBackoffSec: { min: 5, max: 60 },
|
|
29355
29387
|
autoUpdateHour: { min: 0, max: 23 }
|
|
29356
29388
|
};
|
|
29389
|
+
var orgAliasSchema = external_exports.string().regex(/^([a-z0-9-]{3,40})?$/, "alias must be 3\u201340 chars of a\u2013z, 0\u20139, -");
|
|
29357
29390
|
var updateOrgRequestSchema = external_exports.object({
|
|
29358
29391
|
name: external_exports.string().min(1).max(100).optional(),
|
|
29392
|
+
// ADMIN-only login alias (ADR-0111); empty string clears it.
|
|
29393
|
+
alias: orgAliasSchema.optional(),
|
|
29359
29394
|
settings: external_exports.record(external_exports.unknown()).optional()
|
|
29360
29395
|
});
|
|
29361
29396
|
|
|
29362
29397
|
// ../../22-packages/11-shared/31-dto/src/user.ts
|
|
29363
29398
|
var AVATAR_MAX_BYTES = 2 * 1024 * 1024;
|
|
29399
|
+
var USAGE_ALERT_METRICS = ["session5h", "weekly", "monthlyTokens"];
|
|
29400
|
+
var usageAlertRuleSchema = external_exports.object({
|
|
29401
|
+
id: external_exports.string().min(1).max(64),
|
|
29402
|
+
metric: external_exports.enum(USAGE_ALERT_METRICS),
|
|
29403
|
+
threshold: external_exports.number().int().min(1),
|
|
29404
|
+
email: external_exports.union([emailSchema, external_exports.literal("")]).optional(),
|
|
29405
|
+
enabled: external_exports.boolean().default(true)
|
|
29406
|
+
});
|
|
29407
|
+
var userAlertsSchema = external_exports.object({
|
|
29408
|
+
rules: external_exports.array(usageAlertRuleSchema).max(20).default([])
|
|
29409
|
+
});
|
|
29364
29410
|
var rolesSchema = external_exports.array(external_exports.enum(ALL_ROLES)).min(1).refine((roles) => isValidRoleSet(roles), {
|
|
29365
29411
|
message: "ADMIN/MACHINE cannot be combined with other roles"
|
|
29366
29412
|
});
|
|
@@ -29382,7 +29428,11 @@ var updateUserRequestSchema = external_exports.object({
|
|
|
29382
29428
|
email: emailSchema.optional(),
|
|
29383
29429
|
phone: phoneSchema.nullable().optional(),
|
|
29384
29430
|
roles: rolesSchema.optional(),
|
|
29385
|
-
ipAllowlist: ipAllowlistSchema.optional()
|
|
29431
|
+
ipAllowlist: ipAllowlistSchema.optional(),
|
|
29432
|
+
// Usage-alert rules (ADR-0110) — manager-only, like the IP allowlist.
|
|
29433
|
+
alerts: userAlertsSchema.optional(),
|
|
29434
|
+
// cli data retention days (ADR-0115) — manager-only; capped at the plan's retentionDays.
|
|
29435
|
+
cliRetentionDays: external_exports.number().int().min(0).max(3650).optional()
|
|
29386
29436
|
});
|
|
29387
29437
|
var setUserPasswordRequestSchema = external_exports.object({
|
|
29388
29438
|
password: passwordSchema
|
|
@@ -29421,6 +29471,9 @@ var attachTeamUserRequestSchema = external_exports.object({
|
|
|
29421
29471
|
isLead: external_exports.boolean().optional()
|
|
29422
29472
|
});
|
|
29423
29473
|
|
|
29474
|
+
// ../../22-packages/11-shared/31-dto/src/template.ts
|
|
29475
|
+
var TEMPLATE_KINDS = ["report", "est", "wbs", "proposal", "feasibility"];
|
|
29476
|
+
|
|
29424
29477
|
// ../../22-packages/11-shared/31-dto/src/project.ts
|
|
29425
29478
|
var listProjectsQuerySchema = baseRequestSchema.extend({
|
|
29426
29479
|
pinned: pinnedFilterSchema,
|
|
@@ -29485,6 +29538,17 @@ var putSpecDraftRequestSchema = external_exports.object({
|
|
|
29485
29538
|
spec: external_exports.record(external_exports.unknown()),
|
|
29486
29539
|
specVersion: external_exports.number().int().positive().optional()
|
|
29487
29540
|
});
|
|
29541
|
+
var putProjectSpecRequestSchema = external_exports.object({
|
|
29542
|
+
spec: external_exports.record(external_exports.unknown())
|
|
29543
|
+
});
|
|
29544
|
+
var ARTIFACT_MAX_BYTES = 512 * 1024;
|
|
29545
|
+
var listArtifactsQuerySchema = external_exports.object({
|
|
29546
|
+
kind: external_exports.enum(TEMPLATE_KINDS).optional()
|
|
29547
|
+
});
|
|
29548
|
+
var createArtifactRequestSchema = external_exports.object({
|
|
29549
|
+
kind: external_exports.enum(TEMPLATE_KINDS),
|
|
29550
|
+
content: external_exports.string().min(1).max(ARTIFACT_MAX_BYTES)
|
|
29551
|
+
});
|
|
29488
29552
|
var repoSpecSchema = external_exports.object({
|
|
29489
29553
|
/** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
|
|
29490
29554
|
role: external_exports.string().max(60).optional().default(""),
|
|
@@ -29556,10 +29620,26 @@ var createFromSpecRequestSchema = external_exports.object({
|
|
|
29556
29620
|
});
|
|
29557
29621
|
var addRepoRequestSchema = external_exports.object({
|
|
29558
29622
|
projectId: external_exports.string().uuid(),
|
|
29559
|
-
|
|
29560
|
-
|
|
29561
|
-
|
|
29562
|
-
|
|
29623
|
+
repos: external_exports.array(repoSpecSchema).min(1)
|
|
29624
|
+
}).superRefine((v, ctx) => {
|
|
29625
|
+
const primaries = v.repos.filter((r) => r.primary).length;
|
|
29626
|
+
if (primaries !== 1) {
|
|
29627
|
+
ctx.addIssue({
|
|
29628
|
+
code: external_exports.ZodIssueCode.custom,
|
|
29629
|
+
path: ["repos"],
|
|
29630
|
+
message: "Exactly one repo must be primary"
|
|
29631
|
+
});
|
|
29632
|
+
}
|
|
29633
|
+
for (let i = 0; i < v.repos.length; i++) {
|
|
29634
|
+
const r = v.repos[i];
|
|
29635
|
+
if (r && r.op === "existing" && !r.url) {
|
|
29636
|
+
ctx.addIssue({
|
|
29637
|
+
code: external_exports.ZodIssueCode.custom,
|
|
29638
|
+
path: ["repos", i, "url"],
|
|
29639
|
+
message: "An existing repo requires a url"
|
|
29640
|
+
});
|
|
29641
|
+
}
|
|
29642
|
+
}
|
|
29563
29643
|
});
|
|
29564
29644
|
|
|
29565
29645
|
// ../../22-packages/11-shared/31-dto/src/activity.ts
|
|
@@ -29659,7 +29739,11 @@ var dispatchCommandRequestSchema = external_exports.object({
|
|
|
29659
29739
|
}
|
|
29660
29740
|
});
|
|
29661
29741
|
var listCommandsQuerySchema = baseRequestSchema.extend({
|
|
29662
|
-
projectId: external_exports.string().uuid()
|
|
29742
|
+
projectId: external_exports.string().uuid(),
|
|
29743
|
+
// Optional search filters (command-0005): `search` (from BaseRequest) matches the command
|
|
29744
|
+
// text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`.
|
|
29745
|
+
from: external_exports.string().optional(),
|
|
29746
|
+
to: external_exports.string().optional()
|
|
29663
29747
|
});
|
|
29664
29748
|
|
|
29665
29749
|
// ../../22-packages/11-shared/31-dto/src/invitation.ts
|
|
@@ -29757,8 +29841,33 @@ var planEntitlementsSchema = external_exports.object({
|
|
|
29757
29841
|
/** Allowed to use Community/4rum. */
|
|
29758
29842
|
community: external_exports.boolean().default(false),
|
|
29759
29843
|
/** Retention cap (days) for log/command/chat; null = unlimited. */
|
|
29760
|
-
retentionDays: external_exports.number().int().min(1).nullable().default(null)
|
|
29844
|
+
retentionDays: external_exports.number().int().min(1).nullable().default(null),
|
|
29845
|
+
/**
|
|
29846
|
+
* Total 4PM-hosted storage per org (bytes). Originally the template-file cap (ADR-0113),
|
|
29847
|
+
* broadened by ADR-0122 to the org's WHOLE hosted storage: messages + command history
|
|
29848
|
+
* (cli input/output/log) + files (templates/attachments/artifacts). `null` = unlimited.
|
|
29849
|
+
*/
|
|
29850
|
+
templateStorageBytes: external_exports.number().int().min(0).nullable().default(null),
|
|
29851
|
+
/** Per-file cap for a template upload (bytes — ADR-0113); null = unlimited. */
|
|
29852
|
+
templateFileMaxBytes: external_exports.number().int().min(0).nullable().default(null),
|
|
29853
|
+
/**
|
|
29854
|
+
* How many 4PM-hosted machine-users the org may rent (ADR-0126). Base plans grant `0`;
|
|
29855
|
+
* raised only by the `rented_machine` add-on (ADR-0123/0124). Never `null` (not "unlimited").
|
|
29856
|
+
*/
|
|
29857
|
+
rentedMachineSeats: external_exports.number().int().min(0).default(0)
|
|
29761
29858
|
});
|
|
29859
|
+
var DEFAULT_ENTITLEMENTS = {
|
|
29860
|
+
maxUsers: 5,
|
|
29861
|
+
maxProjects: 1,
|
|
29862
|
+
teams: false,
|
|
29863
|
+
community: false,
|
|
29864
|
+
retentionDays: 30,
|
|
29865
|
+
// Free tier: 100 MB total, 10 MB per file (ADR-0113).
|
|
29866
|
+
templateStorageBytes: 100 * 1024 * 1024,
|
|
29867
|
+
templateFileMaxBytes: 10 * 1024 * 1024,
|
|
29868
|
+
// No rented machine-user seats by default — bought via the `rented_machine` add-on (ADR-0126).
|
|
29869
|
+
rentedMachineSeats: 0
|
|
29870
|
+
};
|
|
29762
29871
|
var checkoutRequestSchema = external_exports.object({
|
|
29763
29872
|
planCode: external_exports.string().min(1).max(50),
|
|
29764
29873
|
provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
|
|
@@ -29774,6 +29883,16 @@ var assignPlanRequestSchema = external_exports.object({
|
|
|
29774
29883
|
/** Cycle length in days (defaults to 30). */
|
|
29775
29884
|
periodDays: external_exports.number().int().min(1).max(366).default(30)
|
|
29776
29885
|
});
|
|
29886
|
+
var addonBuyRequestSchema = external_exports.object({
|
|
29887
|
+
code: external_exports.string().min(1).max(50),
|
|
29888
|
+
quantity: external_exports.number().int().min(1).max(1e3),
|
|
29889
|
+
/** Card path only; ignored when the org pays by credit (ADR-0104/0123). */
|
|
29890
|
+
provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
|
|
29891
|
+
});
|
|
29892
|
+
var addonChangeRequestSchema = external_exports.object({
|
|
29893
|
+
quantity: external_exports.number().int().min(0).max(1e3),
|
|
29894
|
+
provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
|
|
29895
|
+
});
|
|
29777
29896
|
|
|
29778
29897
|
// ../../22-packages/11-shared/31-dto/src/search.ts
|
|
29779
29898
|
var SearchHitType = {
|
|
@@ -29787,11 +29906,11 @@ var SearchHitType = {
|
|
|
29787
29906
|
MILESTONE: "milestone",
|
|
29788
29907
|
/** Page/wiki (confluence + project docs — community). */
|
|
29789
29908
|
PAGE: "page",
|
|
29790
|
-
/**
|
|
29909
|
+
/** A message in a channel (Teams channel — community). */
|
|
29791
29910
|
MESSAGE: "message",
|
|
29792
|
-
/**
|
|
29911
|
+
/** A direct/group message (chat history — community). */
|
|
29793
29912
|
CHAT: "chat",
|
|
29794
|
-
/**
|
|
29913
|
+
/** A forum post (community news/forum). */
|
|
29795
29914
|
POST: "post"
|
|
29796
29915
|
};
|
|
29797
29916
|
var searchQuerySchema = external_exports.object({
|
|
@@ -29857,8 +29976,14 @@ var WsChannels = {
|
|
|
29857
29976
|
MACHINE_USAGE: "machine.usage",
|
|
29858
29977
|
// cli → server: durable per-command history record on finish (ADR-0072)
|
|
29859
29978
|
COMMAND_HISTORY: "command.history",
|
|
29979
|
+
// cli → server: periodic upload of the cli's own JSONL log file (ADR-0122) — stored
|
|
29980
|
+
// server-side + counted in the machine user's storage footprint.
|
|
29981
|
+
MACHINE_LOG: "machine.log",
|
|
29860
29982
|
// server → cli (reply): tail the cli's own JSONL logs on demand (ADR-0072)
|
|
29861
29983
|
LOG_READ: "log.read",
|
|
29984
|
+
// server → cli (reply): read a finished command's captured output on demand (ADR-0115) —
|
|
29985
|
+
// cli stores command-output/{commandId}.log; returns null when pruned/unavailable.
|
|
29986
|
+
COMMAND_OUTPUT_READ: "command.output-read",
|
|
29862
29987
|
// outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
|
|
29863
29988
|
REVIEW_REQUEST: "review.request",
|
|
29864
29989
|
REVIEW_EVALUATE: "review.evaluate",
|
|
@@ -29987,7 +30112,7 @@ import { spawn } from "child_process";
|
|
|
29987
30112
|
|
|
29988
30113
|
// src/common/logger/logger.ts
|
|
29989
30114
|
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
29990
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
30115
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
29991
30116
|
import { join as join6 } from "path";
|
|
29992
30117
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
29993
30118
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -30006,6 +30131,13 @@ var CliLogger = class {
|
|
|
30006
30131
|
this.retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
30007
30132
|
void this.cleanupOldLogs();
|
|
30008
30133
|
}
|
|
30134
|
+
/** Update the retention window at runtime + prune now (ADR-0115). `days<=0` keeps current. */
|
|
30135
|
+
setRetentionDays(days) {
|
|
30136
|
+
if (days > 0) {
|
|
30137
|
+
this.retentionDays = days;
|
|
30138
|
+
void this.cleanupOldLogs();
|
|
30139
|
+
}
|
|
30140
|
+
}
|
|
30009
30141
|
/** Log an error-level event. */
|
|
30010
30142
|
error(event, fields) {
|
|
30011
30143
|
this.write("error", event, fields);
|
|
@@ -30062,6 +30194,24 @@ function readRecentLogLines(dir2, limit) {
|
|
|
30062
30194
|
return [];
|
|
30063
30195
|
}
|
|
30064
30196
|
}
|
|
30197
|
+
var LOG_UPLOAD_CONTENT_MAX = 512 * 1024;
|
|
30198
|
+
function readLogUpload(dir2) {
|
|
30199
|
+
try {
|
|
30200
|
+
const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
|
|
30201
|
+
const latest = files[files.length - 1];
|
|
30202
|
+
if (!latest) return null;
|
|
30203
|
+
let totalBytes = 0;
|
|
30204
|
+
for (const f of files) totalBytes += statSync(join6(dir2, f)).size;
|
|
30205
|
+
let content = readFileSync6(join6(dir2, latest), "utf8");
|
|
30206
|
+
if (content.length > LOG_UPLOAD_CONTENT_MAX) {
|
|
30207
|
+
content = `\u2026[truncated]
|
|
30208
|
+
${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
|
|
30209
|
+
}
|
|
30210
|
+
return { fileName: latest, content, totalBytes };
|
|
30211
|
+
} catch {
|
|
30212
|
+
return null;
|
|
30213
|
+
}
|
|
30214
|
+
}
|
|
30065
30215
|
|
|
30066
30216
|
// src/core/update-scheduler.ts
|
|
30067
30217
|
var TICK_MS = 6e4;
|
|
@@ -30320,7 +30470,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
|
30320
30470
|
handlers.onChunk(text);
|
|
30321
30471
|
}
|
|
30322
30472
|
};
|
|
30323
|
-
finalExit = await runAttempt(commandId, plan.cmd,
|
|
30473
|
+
finalExit = await runAttempt(commandId, plan.cmd, attempt.args, cwd2, attempt.env, emit);
|
|
30324
30474
|
const tail = parser.flush();
|
|
30325
30475
|
if (tail) {
|
|
30326
30476
|
captured += tail;
|
|
@@ -30380,13 +30530,18 @@ function resolveHomePath(value) {
|
|
|
30380
30530
|
if (isAbsolute(value)) return value;
|
|
30381
30531
|
return join8(homedir3(), value);
|
|
30382
30532
|
}
|
|
30383
|
-
function
|
|
30384
|
-
if (!
|
|
30385
|
-
|
|
30386
|
-
|
|
30533
|
+
function profileDirs(profiles) {
|
|
30534
|
+
if (!profiles) return [];
|
|
30535
|
+
return profiles.map((p) => p.profile).filter((v) => v && v.trim()).map(resolveHomePath);
|
|
30536
|
+
}
|
|
30537
|
+
function buildRunArgs(profile, cmd, prompt) {
|
|
30538
|
+
const pre = profile.args ?? DEFAULT_AI_ARGS[cmd] ?? [];
|
|
30539
|
+
const model = profile.model?.trim();
|
|
30540
|
+
const modelArgs = model ? ["--model", model] : [];
|
|
30541
|
+
return [...pre, ...modelArgs, prompt];
|
|
30387
30542
|
}
|
|
30388
|
-
function
|
|
30389
|
-
const dirs =
|
|
30543
|
+
function claudeHomeDirs(config) {
|
|
30544
|
+
const dirs = profileDirs(config.claudeHome);
|
|
30390
30545
|
return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
|
|
30391
30546
|
}
|
|
30392
30547
|
function profileDisplayLabel(dir2) {
|
|
@@ -30403,28 +30558,40 @@ function profileEnvVar(cmd) {
|
|
|
30403
30558
|
if (cmd.includes("codex")) return "CODEX_HOME";
|
|
30404
30559
|
return null;
|
|
30405
30560
|
}
|
|
30561
|
+
function profilesFor(config, cmd) {
|
|
30562
|
+
const envVar = profileEnvVar(cmd);
|
|
30563
|
+
if (envVar === "CLAUDE_CONFIG_DIR") return config.claudeHome ?? [];
|
|
30564
|
+
if (envVar === "CODEX_HOME") return config.codexHome ?? [];
|
|
30565
|
+
return [];
|
|
30566
|
+
}
|
|
30406
30567
|
function profileLabels(config) {
|
|
30407
30568
|
const cmd = config.aiCli ?? DEFAULT_AI_CLI;
|
|
30408
|
-
|
|
30409
|
-
const dirs = envVar === "CLAUDE_CONFIG_DIR" ? resolveDirs(config.claudeConfigDir) : envVar === "CODEX_HOME" ? resolveDirs(config.codexHome) : [];
|
|
30410
|
-
return dirs.map((dir2) => profileDisplayLabel(dir2));
|
|
30569
|
+
return profileDirs(profilesFor(config, cmd)).map((dir2) => profileDisplayLabel(dir2));
|
|
30411
30570
|
}
|
|
30412
30571
|
function planAiRun(prompt, config, workingDir) {
|
|
30413
30572
|
const cmd = config.aiCli ?? DEFAULT_AI_CLI;
|
|
30414
|
-
const args = [...config.aiArgs ?? DEFAULT_AI_ARGS[cmd] ?? [], prompt];
|
|
30415
30573
|
const baseEnv = config.aiEnv ?? {};
|
|
30416
30574
|
const envVar = profileEnvVar(cmd);
|
|
30417
|
-
const
|
|
30418
|
-
|
|
30419
|
-
|
|
30420
|
-
|
|
30421
|
-
|
|
30422
|
-
|
|
30575
|
+
const defaultPlan = {
|
|
30576
|
+
cmd,
|
|
30577
|
+
attempts: [
|
|
30578
|
+
{ label: "default", dir: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
|
|
30579
|
+
]
|
|
30580
|
+
};
|
|
30581
|
+
if (!envVar) return defaultPlan;
|
|
30582
|
+
const resolved = profilesFor(config, cmd).filter((p) => p.profile && p.profile.trim()).map((p) => ({ profile: p, dir: resolveHomePath(p.profile) }));
|
|
30583
|
+
if (resolved.length === 0) return defaultPlan;
|
|
30584
|
+
const ordered = workingDir && resolved.some((r) => r.dir === workingDir) ? [
|
|
30585
|
+
resolved.find((r) => r.dir === workingDir),
|
|
30586
|
+
...resolved.filter((r) => r.dir !== workingDir)
|
|
30587
|
+
] : resolved;
|
|
30588
|
+
const attempts = ordered.map(({ profile, dir: dir2 }) => ({
|
|
30423
30589
|
label: profileDisplayLabel(dir2),
|
|
30424
30590
|
dir: dir2,
|
|
30591
|
+
args: buildRunArgs(profile, cmd, prompt),
|
|
30425
30592
|
env: { ...baseEnv, [envVar]: dir2 }
|
|
30426
30593
|
}));
|
|
30427
|
-
return { cmd,
|
|
30594
|
+
return { cmd, attempts };
|
|
30428
30595
|
}
|
|
30429
30596
|
|
|
30430
30597
|
// src/core/claude-usage.ts
|
|
@@ -30779,6 +30946,13 @@ function recordCommand(entry) {
|
|
|
30779
30946
|
function getCommandHistory() {
|
|
30780
30947
|
return entries;
|
|
30781
30948
|
}
|
|
30949
|
+
function pruneCommandHistoryByAge(days) {
|
|
30950
|
+
if (days <= 0) return;
|
|
30951
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
30952
|
+
const before = entries.length;
|
|
30953
|
+
entries = entries.filter((e) => new Date(e.startedAt).getTime() >= cutoff);
|
|
30954
|
+
if (entries.length !== before) persist();
|
|
30955
|
+
}
|
|
30782
30956
|
function finishCommand(commandId, exitCode, usage) {
|
|
30783
30957
|
const entry = entries.find((e) => e.commandId === commandId && e.status === "running");
|
|
30784
30958
|
if (!entry) return;
|
|
@@ -30806,7 +30980,7 @@ import {
|
|
|
30806
30980
|
readFileSync as readFileSync11,
|
|
30807
30981
|
readdirSync as readdirSync3,
|
|
30808
30982
|
rmSync as rmSync3,
|
|
30809
|
-
statSync
|
|
30983
|
+
statSync as statSync2
|
|
30810
30984
|
} from "fs";
|
|
30811
30985
|
import { join as join11 } from "path";
|
|
30812
30986
|
var KEEP_FILES = 200;
|
|
@@ -30839,11 +31013,22 @@ function readCommandOutput(commandId) {
|
|
|
30839
31013
|
function prune() {
|
|
30840
31014
|
if (!dir) return;
|
|
30841
31015
|
try {
|
|
30842
|
-
const files = readdirSync3(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t:
|
|
31016
|
+
const files = readdirSync3(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync2(join11(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
|
|
30843
31017
|
for (const { f } of files.slice(KEEP_FILES)) rmSync3(join11(dir, f), { force: true });
|
|
30844
31018
|
} catch {
|
|
30845
31019
|
}
|
|
30846
31020
|
}
|
|
31021
|
+
function pruneCommandOutputByAge(days) {
|
|
31022
|
+
if (!dir || days <= 0) return;
|
|
31023
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
31024
|
+
try {
|
|
31025
|
+
for (const f of readdirSync3(dir).filter((n) => n.endsWith(".log"))) {
|
|
31026
|
+
const p = join11(dir, f);
|
|
31027
|
+
if (statSync2(p).mtimeMs < cutoff) rmSync3(p, { force: true });
|
|
31028
|
+
}
|
|
31029
|
+
} catch {
|
|
31030
|
+
}
|
|
31031
|
+
}
|
|
30847
31032
|
|
|
30848
31033
|
// src/core/fs-browse.ts
|
|
30849
31034
|
import { readdir as readdir2 } from "fs/promises";
|
|
@@ -31087,9 +31272,14 @@ function subDirName(repo) {
|
|
|
31087
31272
|
const raw = (repo.subdir || repo.role || repo.name || "repo").trim();
|
|
31088
31273
|
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
31089
31274
|
}
|
|
31090
|
-
async function provisionRepos(target, repos, emit) {
|
|
31275
|
+
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
31091
31276
|
for (const repo of repos) {
|
|
31092
31277
|
if (repo.primary) {
|
|
31278
|
+
if (opts.clonePrimary && repo.op === "existing" && repo.url) {
|
|
31279
|
+
emit("git", `Cloning ${repo.url}\u2026`);
|
|
31280
|
+
await run6("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
31281
|
+
continue;
|
|
31282
|
+
}
|
|
31093
31283
|
emit("git", "Initializing the primary repo\u2026");
|
|
31094
31284
|
await run6("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
31095
31285
|
if (repo.op === "existing" && repo.url) {
|
|
@@ -31176,19 +31366,15 @@ async function generateFile(path, prompt) {
|
|
|
31176
31366
|
} catch {
|
|
31177
31367
|
}
|
|
31178
31368
|
}
|
|
31179
|
-
async function addProject(payload, onProgress) {
|
|
31369
|
+
async function addProject(payload, profileDir2, onProgress) {
|
|
31180
31370
|
const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
|
|
31181
31371
|
try {
|
|
31182
|
-
const
|
|
31372
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
|
|
31373
|
+
const target = join12(root, payload.projectName);
|
|
31183
31374
|
await mkdir2(target, { recursive: true });
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
} else {
|
|
31188
|
-
emit("init", "git init\u2026");
|
|
31189
|
-
await run6("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
31190
|
-
}
|
|
31191
|
-
onProgress?.({ projectId: payload.projectId, step: "done", message: "Repo ready.", done: true });
|
|
31375
|
+
const repos = payload.repos ?? [];
|
|
31376
|
+
if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
|
|
31377
|
+
onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
|
|
31192
31378
|
return { ok: true, path: target };
|
|
31193
31379
|
} catch (err) {
|
|
31194
31380
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -31198,9 +31384,10 @@ async function addProject(payload, onProgress) {
|
|
|
31198
31384
|
// src/core/ws-client.ts
|
|
31199
31385
|
var HEARTBEAT_MS = 3e4;
|
|
31200
31386
|
var USAGE_POLL_MS = 5 * 6e4;
|
|
31387
|
+
var LOG_UPLOAD_MS = 10 * 6e4;
|
|
31201
31388
|
var RECONNECT_BACKOFF_BASE_MS = 1e3;
|
|
31202
|
-
var RECONNECT_MAX_BACKOFF_DEFAULT_SEC =
|
|
31203
|
-
var RECONNECT_BACKOFF_MAX_ATTEMPT =
|
|
31389
|
+
var RECONNECT_MAX_BACKOFF_DEFAULT_SEC = 60;
|
|
31390
|
+
var RECONNECT_BACKOFF_MAX_ATTEMPT = 6;
|
|
31204
31391
|
function envReconnectMaxSec() {
|
|
31205
31392
|
const raw = Number(process.env.FOURPM_RECONNECT_MAX_BACKOFF_SEC);
|
|
31206
31393
|
if (!Number.isFinite(raw)) return null;
|
|
@@ -31225,10 +31412,17 @@ var WsClient = class {
|
|
|
31225
31412
|
context;
|
|
31226
31413
|
socket = null;
|
|
31227
31414
|
sessionKey = null;
|
|
31415
|
+
// True once the current connectOnce() reached hello_ack — a *real* session. Gates the
|
|
31416
|
+
// backoff reset so a socket that opens then closes before the handshake keeps backing off
|
|
31417
|
+
// instead of hammering the server every ~1s.
|
|
31418
|
+
sessionEstablished = false;
|
|
31228
31419
|
ecdh = null;
|
|
31229
31420
|
wsToken = "";
|
|
31230
31421
|
heartbeat = null;
|
|
31231
31422
|
usageTimer = null;
|
|
31423
|
+
logUploadTimer = null;
|
|
31424
|
+
/** Signature (fileName + totalBytes) of the last log upload — skip re-sending if unchanged. */
|
|
31425
|
+
lastLogSig = null;
|
|
31232
31426
|
usageSnapshot = null;
|
|
31233
31427
|
/** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
|
|
31234
31428
|
outboundReviewEnabled = false;
|
|
@@ -31253,6 +31447,12 @@ var WsClient = class {
|
|
|
31253
31447
|
connectWaiters = [];
|
|
31254
31448
|
/** Output buffered while disconnected (flushed on reconnect). */
|
|
31255
31449
|
offlineBuffer = [];
|
|
31450
|
+
/**
|
|
31451
|
+
* Command ids already accepted (insertion-ordered, bounded). WS delivery is at-least-once —
|
|
31452
|
+
* a redelivered `COMMAND_DISPATCH` (reconnect/blip during a long AI run) must NOT run again:
|
|
31453
|
+
* re-running an AI prompt would waste tokens and echo the prompt a second time in the console.
|
|
31454
|
+
*/
|
|
31455
|
+
handledCommands = /* @__PURE__ */ new Set();
|
|
31256
31456
|
offlineBufferBytes = 0;
|
|
31257
31457
|
/** cli → server requests awaiting a reply (quota.check — ADR-0020). */
|
|
31258
31458
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -31301,12 +31501,20 @@ var WsClient = class {
|
|
|
31301
31501
|
}
|
|
31302
31502
|
/**
|
|
31303
31503
|
* Resolve the reconnect-backoff cap (ms): the org value (from ws_token) over the
|
|
31304
|
-
* env override over the default. Each is already clamped to the
|
|
31504
|
+
* env override over the default. Each is already clamped to the 1-min ceiling.
|
|
31305
31505
|
*/
|
|
31306
31506
|
reconnectMaxMs() {
|
|
31307
31507
|
const sec = this.orgReconnectMaxSec ?? this.envReconnectMaxSec ?? RECONNECT_MAX_BACKOFF_DEFAULT_SEC;
|
|
31308
31508
|
return sec * 1e3;
|
|
31309
31509
|
}
|
|
31510
|
+
/** Mark a commandId as accepted for dedup, keeping the set bounded (drop the oldest). */
|
|
31511
|
+
markCommandHandled(commandId) {
|
|
31512
|
+
this.handledCommands.add(commandId);
|
|
31513
|
+
if (this.handledCommands.size > 1e3) {
|
|
31514
|
+
const oldest = this.handledCommands.values().next().value;
|
|
31515
|
+
if (oldest !== void 0) this.handledCommands.delete(oldest);
|
|
31516
|
+
}
|
|
31517
|
+
}
|
|
31310
31518
|
/** Backoff sleep that can be woken early by reconnectNow(). */
|
|
31311
31519
|
interruptibleSleep(ms) {
|
|
31312
31520
|
if (this.forceReconnect) {
|
|
@@ -31334,8 +31542,10 @@ var WsClient = class {
|
|
|
31334
31542
|
for (let attempt = 0; !this.stopped; attempt++) {
|
|
31335
31543
|
try {
|
|
31336
31544
|
await this.connectOnce();
|
|
31337
|
-
|
|
31338
|
-
|
|
31545
|
+
if (this.sessionEstablished) {
|
|
31546
|
+
attempt = 0;
|
|
31547
|
+
hashcodeInvalidStreak = 0;
|
|
31548
|
+
}
|
|
31339
31549
|
} catch (err) {
|
|
31340
31550
|
if (err instanceof CliApiError) {
|
|
31341
31551
|
if (["HASHCODE_EXPIRED", "LINK_REVOKED"].includes(err.errorCode)) {
|
|
@@ -31378,6 +31588,7 @@ var WsClient = class {
|
|
|
31378
31588
|
connectOnce() {
|
|
31379
31589
|
return new Promise((resolve6, reject) => {
|
|
31380
31590
|
void (async () => {
|
|
31591
|
+
this.sessionEstablished = false;
|
|
31381
31592
|
this.bus.setStatus("connecting");
|
|
31382
31593
|
const token = await requestWsToken(this.credential.serverUrl, this.credential.hashcode3);
|
|
31383
31594
|
this.wsToken = token.wsToken;
|
|
@@ -31391,6 +31602,11 @@ var WsClient = class {
|
|
|
31391
31602
|
timezone: token.timezone
|
|
31392
31603
|
});
|
|
31393
31604
|
}
|
|
31605
|
+
if (typeof token.cliRetentionDays === "number" && token.cliRetentionDays > 0) {
|
|
31606
|
+
logger.setRetentionDays(token.cliRetentionDays);
|
|
31607
|
+
pruneCommandHistoryByAge(token.cliRetentionDays);
|
|
31608
|
+
pruneCommandOutputByAge(token.cliRetentionDays);
|
|
31609
|
+
}
|
|
31394
31610
|
writeProfileConfig(this.context.profileDir, {
|
|
31395
31611
|
sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
|
|
31396
31612
|
perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
|
|
@@ -31404,7 +31620,12 @@ var WsClient = class {
|
|
|
31404
31620
|
} else if (token.scope) {
|
|
31405
31621
|
this.bus.setScope(token.scope);
|
|
31406
31622
|
}
|
|
31407
|
-
|
|
31623
|
+
if (token.wsUrl !== void 0 && token.wsUrl !== this.credential.wsUrl) {
|
|
31624
|
+
this.credential.wsUrl = token.wsUrl || void 0;
|
|
31625
|
+
writeCredential(this.context.profileDir, this.credential);
|
|
31626
|
+
}
|
|
31627
|
+
const wsBase = this.credential.wsUrl ?? this.credential.serverUrl;
|
|
31628
|
+
const wsUrl = wsBase.replace(/^http/, "ws") + "/ws";
|
|
31408
31629
|
const socket = new wrapper_default(wsUrl);
|
|
31409
31630
|
this.socket = socket;
|
|
31410
31631
|
socket.on("open", () => {
|
|
@@ -31450,6 +31671,7 @@ var WsClient = class {
|
|
|
31450
31671
|
if ("type" in message) {
|
|
31451
31672
|
if (message.type === "hello_ack" && this.ecdh) {
|
|
31452
31673
|
this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
|
|
31674
|
+
this.sessionEstablished = true;
|
|
31453
31675
|
if (message.workerName) this.bus.setWorker(message.workerName);
|
|
31454
31676
|
if (message.projectName) {
|
|
31455
31677
|
this.bus.setProject(message.projectName);
|
|
@@ -31466,6 +31688,7 @@ var WsClient = class {
|
|
|
31466
31688
|
this.flushOfflineBuffer();
|
|
31467
31689
|
this.startHeartbeat();
|
|
31468
31690
|
this.startUsagePolling();
|
|
31691
|
+
this.startLogUpload();
|
|
31469
31692
|
this.connectWaiters.splice(0).forEach((fn) => fn());
|
|
31470
31693
|
return;
|
|
31471
31694
|
}
|
|
@@ -31542,6 +31765,11 @@ var WsClient = class {
|
|
|
31542
31765
|
}
|
|
31543
31766
|
case WsChannels.COMMAND_DISPATCH: {
|
|
31544
31767
|
const dispatch = payload;
|
|
31768
|
+
if (this.handledCommands.has(dispatch.commandId)) {
|
|
31769
|
+
logger.info("command.dispatch.duplicate", { commandId: dispatch.commandId });
|
|
31770
|
+
break;
|
|
31771
|
+
}
|
|
31772
|
+
this.markCommandHandled(dispatch.commandId);
|
|
31545
31773
|
logger.info("command.dispatch", {
|
|
31546
31774
|
commandId: dispatch.commandId,
|
|
31547
31775
|
cmd: dispatch.cmd,
|
|
@@ -31635,6 +31863,11 @@ var WsClient = class {
|
|
|
31635
31863
|
this.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
31636
31864
|
break;
|
|
31637
31865
|
}
|
|
31866
|
+
case WsChannels.COMMAND_OUTPUT_READ: {
|
|
31867
|
+
const output = readCommandOutput(payload.commandId);
|
|
31868
|
+
this.send(WsChannels.COMMAND_OUTPUT_READ, { output }, message.id);
|
|
31869
|
+
break;
|
|
31870
|
+
}
|
|
31638
31871
|
case WsChannels.PROJECT_CREATE:
|
|
31639
31872
|
void scaffoldProject(
|
|
31640
31873
|
payload,
|
|
@@ -31645,6 +31878,7 @@ var WsClient = class {
|
|
|
31645
31878
|
case WsChannels.PROJECT_ADD:
|
|
31646
31879
|
void addProject(
|
|
31647
31880
|
payload,
|
|
31881
|
+
this.context.profileDir,
|
|
31648
31882
|
(p) => this.send(WsChannels.PROJECT_PROGRESS, p)
|
|
31649
31883
|
).then((reply) => this.send(WsChannels.PROJECT_ADD, reply, message.id));
|
|
31650
31884
|
break;
|
|
@@ -31736,17 +31970,18 @@ var WsClient = class {
|
|
|
31736
31970
|
});
|
|
31737
31971
|
}
|
|
31738
31972
|
const plan = planAiRun(prompt, config, startDir ?? workingDir);
|
|
31973
|
+
const markerArgs = plan.attempts[0]?.args ?? [];
|
|
31739
31974
|
this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
|
|
31740
31975
|
logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
|
|
31741
31976
|
if (origin === "local") {
|
|
31742
31977
|
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
31743
31978
|
commandId,
|
|
31744
31979
|
cmd: plan.cmd,
|
|
31745
|
-
args:
|
|
31980
|
+
args: markerArgs,
|
|
31746
31981
|
origin: "local"
|
|
31747
31982
|
});
|
|
31748
31983
|
}
|
|
31749
|
-
recordCommand({ commandId, cmd: plan.cmd, args:
|
|
31984
|
+
recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
|
|
31750
31985
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31751
31986
|
this.bus.startBusy(plan.cmd);
|
|
31752
31987
|
let seq = 0;
|
|
@@ -31811,7 +32046,7 @@ var WsClient = class {
|
|
|
31811
32046
|
this.send(WsChannels.COMMAND_HISTORY, {
|
|
31812
32047
|
commandId,
|
|
31813
32048
|
cmd: plan.cmd,
|
|
31814
|
-
args:
|
|
32049
|
+
args: markerArgs,
|
|
31815
32050
|
status: result.exitCode === 0 ? "done" : "failed",
|
|
31816
32051
|
exitCode: result.exitCode,
|
|
31817
32052
|
tokens: result.usage.tokens,
|
|
@@ -31847,7 +32082,7 @@ var WsClient = class {
|
|
|
31847
32082
|
if (threshold <= 0 || cmd !== "claude") return null;
|
|
31848
32083
|
const util2 = this.usageSnapshot?.session.utilizationPct ?? 0;
|
|
31849
32084
|
if (util2 < threshold) return null;
|
|
31850
|
-
const dirs =
|
|
32085
|
+
const dirs = claudeHomeDirs(config);
|
|
31851
32086
|
if (dirs.length < 2) return null;
|
|
31852
32087
|
const currentIndex = workingDir ? dirs.indexOf(workingDir) : -1;
|
|
31853
32088
|
const next = dirs[(currentIndex + 1) % dirs.length];
|
|
@@ -32048,6 +32283,8 @@ var WsClient = class {
|
|
|
32048
32283
|
this.heartbeat = null;
|
|
32049
32284
|
if (this.usageTimer) clearInterval(this.usageTimer);
|
|
32050
32285
|
this.usageTimer = null;
|
|
32286
|
+
if (this.logUploadTimer) clearInterval(this.logUploadTimer);
|
|
32287
|
+
this.logUploadTimer = null;
|
|
32051
32288
|
}
|
|
32052
32289
|
/**
|
|
32053
32290
|
* Poll the Claude subscription usage API and push a `machine.usage` snapshot (ADR-0072).
|
|
@@ -32060,7 +32297,7 @@ var WsClient = class {
|
|
|
32060
32297
|
const config = readProfileConfig(this.context.profileDir);
|
|
32061
32298
|
const aiCli2 = config.aiCli ?? "claude";
|
|
32062
32299
|
const working = getWorkingProfile(this.context.profileDir, aiCli2);
|
|
32063
|
-
const configured =
|
|
32300
|
+
const configured = claudeHomeDirs(config);
|
|
32064
32301
|
const dirs = working ? [working, ...configured.filter((d) => d !== working)] : configured;
|
|
32065
32302
|
const profile = working ? basename3(working) : dirs[0] ? basename3(dirs[0]) : "default";
|
|
32066
32303
|
const api = await checkClaudeUsage(dirs, aiCli2);
|
|
@@ -32085,6 +32322,29 @@ var WsClient = class {
|
|
|
32085
32322
|
this.usageTimer.unref?.();
|
|
32086
32323
|
void this.pollUsage();
|
|
32087
32324
|
}
|
|
32325
|
+
/** Start the periodic upload of the cli's own JSONL log to the server (ADR-0122). */
|
|
32326
|
+
startLogUpload() {
|
|
32327
|
+
if (this.logUploadTimer) clearInterval(this.logUploadTimer);
|
|
32328
|
+
this.logUploadTimer = setInterval(() => this.uploadLog(), LOG_UPLOAD_MS);
|
|
32329
|
+
this.logUploadTimer.unref?.();
|
|
32330
|
+
this.uploadLog();
|
|
32331
|
+
}
|
|
32332
|
+
/**
|
|
32333
|
+
* Upload the newest cli log file + the total log footprint to the server (ADR-0122), so it is
|
|
32334
|
+
* stored + retained server-side and counted in the machine user's storage. Skips when nothing
|
|
32335
|
+
* changed since the last upload. Best-effort — never disrupts the session.
|
|
32336
|
+
*/
|
|
32337
|
+
uploadLog() {
|
|
32338
|
+
try {
|
|
32339
|
+
const upload = readLogUpload(join13(this.context.profileDir, "logs"));
|
|
32340
|
+
if (!upload) return;
|
|
32341
|
+
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
32342
|
+
if (sig === this.lastLogSig) return;
|
|
32343
|
+
this.lastLogSig = sig;
|
|
32344
|
+
this.send(WsChannels.MACHINE_LOG, upload);
|
|
32345
|
+
} catch {
|
|
32346
|
+
}
|
|
32347
|
+
}
|
|
32088
32348
|
/**
|
|
32089
32349
|
* Logout: close WS, delete `.cre`, and prompt to pair again (Unlinked state).
|
|
32090
32350
|
*/
|
|
@@ -38258,9 +38518,9 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
38258
38518
|
import { join as join16 } from "path";
|
|
38259
38519
|
var CONFIG_KEYS = [
|
|
38260
38520
|
"aiCli",
|
|
38261
|
-
"
|
|
38262
|
-
"claudeConfigDir",
|
|
38521
|
+
"claudeHome",
|
|
38263
38522
|
"codexHome",
|
|
38523
|
+
"antigravityHome",
|
|
38264
38524
|
"physicPath",
|
|
38265
38525
|
"commandHistoryUploadMinutes",
|
|
38266
38526
|
"autoUpdate",
|
|
@@ -38276,14 +38536,13 @@ function parseConfigAssignment(key, valueTokens) {
|
|
|
38276
38536
|
case "physicPath":
|
|
38277
38537
|
if (!value) return { error: `Missing value for "${key}".` };
|
|
38278
38538
|
return { patch: { [key]: value } };
|
|
38279
|
-
case "
|
|
38280
|
-
case "codexHome":
|
|
38539
|
+
case "claudeHome":
|
|
38540
|
+
case "codexHome":
|
|
38541
|
+
case "antigravityHome": {
|
|
38281
38542
|
const dirs = valueTokens.filter((t) => t.trim());
|
|
38282
38543
|
if (dirs.length === 0) return { error: `Missing value for "${key}".` };
|
|
38283
|
-
return { patch: { [key]: dirs.
|
|
38544
|
+
return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
|
|
38284
38545
|
}
|
|
38285
|
-
case "aiArgs":
|
|
38286
|
-
return { patch: { aiArgs: valueTokens } };
|
|
38287
38546
|
case "commandHistoryUploadMinutes": {
|
|
38288
38547
|
const n = Number(value);
|
|
38289
38548
|
if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
|
|
@@ -38313,22 +38572,19 @@ function runConfig(ctx) {
|
|
|
38313
38572
|
return;
|
|
38314
38573
|
}
|
|
38315
38574
|
if (sub === "init") {
|
|
38575
|
+
const writeDefaults = (verb) => {
|
|
38576
|
+
overwriteProfileConfig(dir2, defaultProfileConfig());
|
|
38577
|
+
ctx.print(`\u2714 ${verb} default config: ${path}`);
|
|
38578
|
+
ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
|
|
38579
|
+
};
|
|
38316
38580
|
if (existsSync10(path)) {
|
|
38317
|
-
ctx.
|
|
38581
|
+
ctx.confirm(
|
|
38582
|
+
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
38583
|
+
() => writeDefaults("replaced with")
|
|
38584
|
+
);
|
|
38318
38585
|
return;
|
|
38319
38586
|
}
|
|
38320
|
-
|
|
38321
|
-
aiCli: "claude",
|
|
38322
|
-
aiArgs: ["-p"],
|
|
38323
|
-
// Lists so failover across profiles is discoverable (ADR-0057) — add more dirs.
|
|
38324
|
-
claudeConfigDir: [".claude"],
|
|
38325
|
-
codexHome: [".codex"],
|
|
38326
|
-
autoUpdate: true,
|
|
38327
|
-
commandHistoryUploadMinutes: 10
|
|
38328
|
-
};
|
|
38329
|
-
writeProfileConfig(dir2, defaults);
|
|
38330
|
-
ctx.print(`\u2714 wrote default config: ${path}`);
|
|
38331
|
-
ctx.print("add claude profiles: /config set claudeConfigDir .claude .claude-1 (tried in order)");
|
|
38587
|
+
writeDefaults("wrote");
|
|
38332
38588
|
return;
|
|
38333
38589
|
}
|
|
38334
38590
|
if (sub === "set") {
|
|
@@ -38729,6 +38985,7 @@ function App2({
|
|
|
38729
38985
|
const [expanded, setExpanded] = (0, import_react26.useState)(() => /* @__PURE__ */ new Set());
|
|
38730
38986
|
const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
|
|
38731
38987
|
const [startedAt] = (0, import_react26.useState)(() => Date.now());
|
|
38988
|
+
const [pendingConfirm, setPendingConfirm] = (0, import_react26.useState)(null);
|
|
38732
38989
|
(0, import_react26.useEffect)(() => {
|
|
38733
38990
|
const offTranscript = bus.onTranscript((entry) => {
|
|
38734
38991
|
setEntries(
|
|
@@ -38785,8 +39042,16 @@ function App2({
|
|
|
38785
39042
|
};
|
|
38786
39043
|
const handleSubmit = (input) => {
|
|
38787
39044
|
setScrollBack(0);
|
|
38788
|
-
setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
|
|
38789
39045
|
const trimmed = input.trim();
|
|
39046
|
+
if (pendingConfirm) {
|
|
39047
|
+
const { onYes } = pendingConfirm;
|
|
39048
|
+
setPendingConfirm(null);
|
|
39049
|
+
bus.push({ source: "local", kind: "cmd", text: trimmed });
|
|
39050
|
+
if (/^y(es)?$/i.test(trimmed)) onYes();
|
|
39051
|
+
else bus.push({ source: "local", kind: "log", text: "cancelled." });
|
|
39052
|
+
return;
|
|
39053
|
+
}
|
|
39054
|
+
setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
|
|
38790
39055
|
if (trimmed.startsWith("/")) {
|
|
38791
39056
|
bus.push({ source: "local", kind: "cmd", text: trimmed });
|
|
38792
39057
|
runSlashCommand(trimmed, {
|
|
@@ -38798,6 +39063,10 @@ function App2({
|
|
|
38798
39063
|
stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
38799
39064
|
},
|
|
38800
39065
|
quit: () => exit(),
|
|
39066
|
+
confirm: (prompt, onYes) => {
|
|
39067
|
+
bus.push({ source: "local", kind: "log", text: prompt });
|
|
39068
|
+
setPendingConfirm({ prompt, onYes });
|
|
39069
|
+
},
|
|
38801
39070
|
submitAi: (aiInput) => bus.submitLocal(aiInput),
|
|
38802
39071
|
reconnect: () => bus.requestReconnect(),
|
|
38803
39072
|
expand: expandFold,
|
|
@@ -38869,6 +39138,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
38869
39138
|
base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
|
|
38870
39139
|
});
|
|
38871
39140
|
logger.info("cli.start", { serverUrl: credential.serverUrl });
|
|
39141
|
+
ensureProfileConfig(profileDir2);
|
|
38872
39142
|
const config = readProfileConfig(profileDir2);
|
|
38873
39143
|
const autoUpdate = config.autoUpdate !== false && process.env.FOURPM_NO_UPDATE !== "1";
|
|
38874
39144
|
const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
|