@4pm/cli 0.5.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 +1229 -199
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1995,14 +1995,14 @@ var require_react_development = __commonJS({
|
|
|
1995
1995
|
var thenableResult = result;
|
|
1996
1996
|
var wasAwaited = false;
|
|
1997
1997
|
var thenable = {
|
|
1998
|
-
then: function(
|
|
1998
|
+
then: function(resolve6, reject) {
|
|
1999
1999
|
wasAwaited = true;
|
|
2000
2000
|
thenableResult.then(function(returnValue2) {
|
|
2001
2001
|
popActScope(prevActScopeDepth);
|
|
2002
2002
|
if (actScopeDepth === 0) {
|
|
2003
|
-
recursivelyFlushAsyncActWork(returnValue2,
|
|
2003
|
+
recursivelyFlushAsyncActWork(returnValue2, resolve6, reject);
|
|
2004
2004
|
} else {
|
|
2005
|
-
|
|
2005
|
+
resolve6(returnValue2);
|
|
2006
2006
|
}
|
|
2007
2007
|
}, function(error2) {
|
|
2008
2008
|
popActScope(prevActScopeDepth);
|
|
@@ -2032,20 +2032,20 @@ var require_react_development = __commonJS({
|
|
|
2032
2032
|
ReactCurrentActQueue.current = null;
|
|
2033
2033
|
}
|
|
2034
2034
|
var _thenable = {
|
|
2035
|
-
then: function(
|
|
2035
|
+
then: function(resolve6, reject) {
|
|
2036
2036
|
if (ReactCurrentActQueue.current === null) {
|
|
2037
2037
|
ReactCurrentActQueue.current = [];
|
|
2038
|
-
recursivelyFlushAsyncActWork(returnValue,
|
|
2038
|
+
recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
|
|
2039
2039
|
} else {
|
|
2040
|
-
|
|
2040
|
+
resolve6(returnValue);
|
|
2041
2041
|
}
|
|
2042
2042
|
}
|
|
2043
2043
|
};
|
|
2044
2044
|
return _thenable;
|
|
2045
2045
|
} else {
|
|
2046
2046
|
var _thenable2 = {
|
|
2047
|
-
then: function(
|
|
2048
|
-
|
|
2047
|
+
then: function(resolve6, reject) {
|
|
2048
|
+
resolve6(returnValue);
|
|
2049
2049
|
}
|
|
2050
2050
|
};
|
|
2051
2051
|
return _thenable2;
|
|
@@ -2061,7 +2061,7 @@ var require_react_development = __commonJS({
|
|
|
2061
2061
|
actScopeDepth = prevActScopeDepth;
|
|
2062
2062
|
}
|
|
2063
2063
|
}
|
|
2064
|
-
function recursivelyFlushAsyncActWork(returnValue,
|
|
2064
|
+
function recursivelyFlushAsyncActWork(returnValue, resolve6, reject) {
|
|
2065
2065
|
{
|
|
2066
2066
|
var queue = ReactCurrentActQueue.current;
|
|
2067
2067
|
if (queue !== null) {
|
|
@@ -2070,16 +2070,16 @@ var require_react_development = __commonJS({
|
|
|
2070
2070
|
enqueueTask(function() {
|
|
2071
2071
|
if (queue.length === 0) {
|
|
2072
2072
|
ReactCurrentActQueue.current = null;
|
|
2073
|
-
|
|
2073
|
+
resolve6(returnValue);
|
|
2074
2074
|
} else {
|
|
2075
|
-
recursivelyFlushAsyncActWork(returnValue,
|
|
2075
|
+
recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
|
|
2076
2076
|
}
|
|
2077
2077
|
});
|
|
2078
2078
|
} catch (error2) {
|
|
2079
2079
|
reject(error2);
|
|
2080
2080
|
}
|
|
2081
2081
|
} else {
|
|
2082
|
-
|
|
2082
|
+
resolve6(returnValue);
|
|
2083
2083
|
}
|
|
2084
2084
|
}
|
|
2085
2085
|
}
|
|
@@ -24450,7 +24450,7 @@ function resolveProfileArg(args) {
|
|
|
24450
24450
|
const rest = [...args.slice(0, index), ...args.slice(index + 2)];
|
|
24451
24451
|
return [name, rest];
|
|
24452
24452
|
}
|
|
24453
|
-
return [process.env.FOURPM_PROFILE
|
|
24453
|
+
return [process.env.FOURPM_PROFILE || null, args];
|
|
24454
24454
|
}
|
|
24455
24455
|
function listProfiles() {
|
|
24456
24456
|
const base = join(homedir(), ".4pm", "profiles");
|
|
@@ -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
|
}
|
|
@@ -24842,6 +24869,30 @@ function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
|
24842
24869
|
const exp = Math.min(baseMs * 2 ** attempt, maxMs);
|
|
24843
24870
|
return Math.floor(exp / 2 + Math.random() * (exp / 2));
|
|
24844
24871
|
}
|
|
24872
|
+
function looksLikeJsonOrCode(text) {
|
|
24873
|
+
const t = text.replace(/^\s+/, "");
|
|
24874
|
+
if (t.startsWith("```")) return "code";
|
|
24875
|
+
if (t.startsWith("{") || t.startsWith("[")) return "json";
|
|
24876
|
+
return null;
|
|
24877
|
+
}
|
|
24878
|
+
function prettyResult(text) {
|
|
24879
|
+
const trimmed = text.trim();
|
|
24880
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
24881
|
+
try {
|
|
24882
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
24883
|
+
} catch {
|
|
24884
|
+
const start = trimmed.search(/[{[]/);
|
|
24885
|
+
const end = Math.max(trimmed.lastIndexOf("}"), trimmed.lastIndexOf("]"));
|
|
24886
|
+
if (start >= 0 && end > start) {
|
|
24887
|
+
try {
|
|
24888
|
+
return JSON.stringify(JSON.parse(trimmed.slice(start, end + 1)), null, 2);
|
|
24889
|
+
} catch {
|
|
24890
|
+
}
|
|
24891
|
+
}
|
|
24892
|
+
}
|
|
24893
|
+
}
|
|
24894
|
+
return trimmed;
|
|
24895
|
+
}
|
|
24845
24896
|
|
|
24846
24897
|
// ../../22-packages/11-shared/11-constants/src/roles.ts
|
|
24847
24898
|
var Role = {
|
|
@@ -24880,6 +24931,8 @@ var PermissionCode = {
|
|
|
24880
24931
|
// ADMIN resets a sub-account's password directly (user-0010)
|
|
24881
24932
|
USER_SET_PASSWORD: "user.set_password",
|
|
24882
24933
|
USER_DELETE: "user.delete",
|
|
24934
|
+
// org-level pause/resume of a user (ADR-0093)
|
|
24935
|
+
USER_PAUSE: "user.pause",
|
|
24883
24936
|
// invite users by email (ADR-0021)
|
|
24884
24937
|
USER_INVITE: "user.invite",
|
|
24885
24938
|
// team
|
|
@@ -24904,6 +24957,11 @@ var PermissionCode = {
|
|
|
24904
24957
|
PROJECT_DETACH_USER: "project.detach_user",
|
|
24905
24958
|
PROJECT_ATTACH_MANAGER: "project.attach_manager",
|
|
24906
24959
|
PROJECT_DETACH_MANAGER: "project.detach_manager",
|
|
24960
|
+
// project lifecycle (ADR-0092): pause+resume / close+reopen
|
|
24961
|
+
PROJECT_PAUSE: "project.pause",
|
|
24962
|
+
PROJECT_CLOSE: "project.close",
|
|
24963
|
+
// project-membership pause+resume of a direct member (ADR-0093)
|
|
24964
|
+
PROJECT_PAUSE_MEMBER: "project.pause_member",
|
|
24907
24965
|
PROJECT_AI_ASSIST: "project.ai_assist",
|
|
24908
24966
|
PROJECT_MEMO_READ: "project.memo_read",
|
|
24909
24967
|
PROJECT_MEMO_WRITE: "project.memo_write",
|
|
@@ -24932,6 +24990,9 @@ var PermissionCode = {
|
|
|
24932
24990
|
// billing
|
|
24933
24991
|
BILLING_READ: "billing.read",
|
|
24934
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",
|
|
24935
24996
|
// integration (community-server — ADR-0058; served by @4pm/community)
|
|
24936
24997
|
INTEGRATION_READ: "integration.read",
|
|
24937
24998
|
INTEGRATION_CONNECT: "integration.connect",
|
|
@@ -24971,9 +25032,12 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
24971
25032
|
P.USER_READ,
|
|
24972
25033
|
P.USER_UPDATE,
|
|
24973
25034
|
P.USER_DELETE,
|
|
25035
|
+
P.USER_PAUSE,
|
|
24974
25036
|
P.USER_INVITE,
|
|
24975
25037
|
P.QUOTA_READ,
|
|
24976
25038
|
P.QUOTA_MANAGE,
|
|
25039
|
+
P.TEMPLATE_READ,
|
|
25040
|
+
P.TEMPLATE_WRITE,
|
|
24977
25041
|
P.TEAM_LIST,
|
|
24978
25042
|
P.TEAM_CREATE,
|
|
24979
25043
|
P.TEAM_READ,
|
|
@@ -24994,6 +25058,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
24994
25058
|
P.PROJECT_DETACH_USER,
|
|
24995
25059
|
P.PROJECT_ATTACH_MANAGER,
|
|
24996
25060
|
P.PROJECT_DETACH_MANAGER,
|
|
25061
|
+
P.PROJECT_PAUSE,
|
|
25062
|
+
P.PROJECT_CLOSE,
|
|
25063
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
24997
25064
|
P.PROJECT_AI_ASSIST,
|
|
24998
25065
|
P.PROJECT_MEMO_READ,
|
|
24999
25066
|
P.PROJECT_MEMO_WRITE,
|
|
@@ -25060,7 +25127,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
25060
25127
|
P.INTEGRATION_PAGE_WRITE
|
|
25061
25128
|
]
|
|
25062
25129
|
};
|
|
25063
|
-
var TL_DEFAULT_PERMISSIONS = ROLE_DEFAULT_PERMISSIONS[Role.PM].filter(
|
|
25130
|
+
var TL_DEFAULT_PERMISSIONS = ROLE_DEFAULT_PERMISSIONS[Role.PM].filter(
|
|
25131
|
+
(code) => code !== P.USER_DELETE && code !== P.USER_PAUSE
|
|
25132
|
+
);
|
|
25064
25133
|
var PROJECT_SCOPE_CHECKED_CODES = [
|
|
25065
25134
|
P.PROJECT_UPDATE,
|
|
25066
25135
|
P.PROJECT_DELETE,
|
|
@@ -25070,6 +25139,9 @@ var PROJECT_SCOPE_CHECKED_CODES = [
|
|
|
25070
25139
|
P.PROJECT_DETACH_USER,
|
|
25071
25140
|
P.PROJECT_ATTACH_MANAGER,
|
|
25072
25141
|
P.PROJECT_DETACH_MANAGER,
|
|
25142
|
+
P.PROJECT_PAUSE,
|
|
25143
|
+
P.PROJECT_CLOSE,
|
|
25144
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
25073
25145
|
P.PROJECT_AI_ASSIST
|
|
25074
25146
|
];
|
|
25075
25147
|
var SELF_SERVICE_CODES = [
|
|
@@ -25104,6 +25176,9 @@ var TEAM_GRANTABLE_CODES = [
|
|
|
25104
25176
|
P.PROJECT_DETACH_USER,
|
|
25105
25177
|
P.PROJECT_ATTACH_MANAGER,
|
|
25106
25178
|
P.PROJECT_DETACH_MANAGER,
|
|
25179
|
+
P.PROJECT_PAUSE,
|
|
25180
|
+
P.PROJECT_CLOSE,
|
|
25181
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
25107
25182
|
P.PROJECT_AI_ASSIST,
|
|
25108
25183
|
P.PROJECT_MEMO_READ,
|
|
25109
25184
|
P.PROJECT_MEMO_WRITE,
|
|
@@ -25122,6 +25197,9 @@ var PROJECT_GRANTABLE_CODES = [
|
|
|
25122
25197
|
P.PROJECT_DETACH_USER,
|
|
25123
25198
|
P.PROJECT_ATTACH_MANAGER,
|
|
25124
25199
|
P.PROJECT_DETACH_MANAGER,
|
|
25200
|
+
P.PROJECT_PAUSE,
|
|
25201
|
+
P.PROJECT_CLOSE,
|
|
25202
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
25125
25203
|
P.PROJECT_AI_ASSIST,
|
|
25126
25204
|
P.PROJECT_MEMO_READ,
|
|
25127
25205
|
P.PROJECT_MEMO_WRITE,
|
|
@@ -25178,6 +25256,12 @@ var QuotaOnExceed = {
|
|
|
25178
25256
|
WARN: "warn"
|
|
25179
25257
|
};
|
|
25180
25258
|
|
|
25259
|
+
// ../../22-packages/11-shared/11-constants/src/billing.ts
|
|
25260
|
+
var PaymentProvider = {
|
|
25261
|
+
STRIPE: "stripe",
|
|
25262
|
+
PAYPAL: "paypal"
|
|
25263
|
+
};
|
|
25264
|
+
|
|
25181
25265
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
25182
25266
|
var external_exports = {};
|
|
25183
25267
|
__export(external_exports, {
|
|
@@ -29228,6 +29312,7 @@ var baseRequestSchema = external_exports.object({
|
|
|
29228
29312
|
order: external_exports.enum(["asc", "desc"]).default("desc")
|
|
29229
29313
|
});
|
|
29230
29314
|
var pinnedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
|
|
29315
|
+
var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
|
|
29231
29316
|
|
|
29232
29317
|
// ../../22-packages/11-shared/41-validation/src/schemas.ts
|
|
29233
29318
|
var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(72, "password: at most 72 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
|
|
@@ -29243,12 +29328,18 @@ var ipAllowlistSchema = external_exports.array(ipCidrSchema).max(50).default([])
|
|
|
29243
29328
|
var hexTokenSchema = external_exports.string().regex(/^[0-9a-f]{64}$/, "token: invalid format");
|
|
29244
29329
|
|
|
29245
29330
|
// ../../22-packages/11-shared/31-dto/src/auth.ts
|
|
29331
|
+
var paymentProviderSchema = external_exports.enum([
|
|
29332
|
+
PaymentProvider.STRIPE,
|
|
29333
|
+
PaymentProvider.PAYPAL
|
|
29334
|
+
]);
|
|
29246
29335
|
var registerRequestSchema = external_exports.object({
|
|
29247
29336
|
username: usernameSchema,
|
|
29248
29337
|
email: emailSchema,
|
|
29249
29338
|
password: passwordSchema,
|
|
29250
29339
|
phone: phoneSchema,
|
|
29251
|
-
ipAllowlist: ipAllowlistSchema.optional()
|
|
29340
|
+
ipAllowlist: ipAllowlistSchema.optional(),
|
|
29341
|
+
planCode: external_exports.string().min(1).max(50).optional(),
|
|
29342
|
+
provider: paymentProviderSchema.optional()
|
|
29252
29343
|
});
|
|
29253
29344
|
var mailTokenRequestSchema = external_exports.object({
|
|
29254
29345
|
token: hexTokenSchema
|
|
@@ -29292,21 +29383,37 @@ var resetPasswordRequestSchema = external_exports.object({
|
|
|
29292
29383
|
|
|
29293
29384
|
// ../../22-packages/11-shared/31-dto/src/org.ts
|
|
29294
29385
|
var CLI_SETTINGS_BOUNDS = {
|
|
29295
|
-
reconnectMaxBackoffSec: { min: 5, max:
|
|
29386
|
+
reconnectMaxBackoffSec: { min: 5, max: 60 },
|
|
29296
29387
|
autoUpdateHour: { min: 0, max: 23 }
|
|
29297
29388
|
};
|
|
29389
|
+
var orgAliasSchema = external_exports.string().regex(/^([a-z0-9-]{3,40})?$/, "alias must be 3\u201340 chars of a\u2013z, 0\u20139, -");
|
|
29298
29390
|
var updateOrgRequestSchema = external_exports.object({
|
|
29299
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(),
|
|
29300
29394
|
settings: external_exports.record(external_exports.unknown()).optional()
|
|
29301
29395
|
});
|
|
29302
29396
|
|
|
29303
29397
|
// ../../22-packages/11-shared/31-dto/src/user.ts
|
|
29304
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
|
+
});
|
|
29305
29410
|
var rolesSchema = external_exports.array(external_exports.enum(ALL_ROLES)).min(1).refine((roles) => isValidRoleSet(roles), {
|
|
29306
29411
|
message: "ADMIN/MACHINE cannot be combined with other roles"
|
|
29307
29412
|
});
|
|
29308
29413
|
var listUsersQuerySchema = baseRequestSchema.extend({
|
|
29309
|
-
role: external_exports.enum(ALL_ROLES).optional()
|
|
29414
|
+
role: external_exports.enum(ALL_ROLES).optional(),
|
|
29415
|
+
/** ADMIN-only Trash: list soft-deleted users (ADR-0109). */
|
|
29416
|
+
deleted: deletedFilterSchema
|
|
29310
29417
|
});
|
|
29311
29418
|
var createUserRequestSchema = external_exports.object({
|
|
29312
29419
|
username: usernameSchema,
|
|
@@ -29321,7 +29428,11 @@ var updateUserRequestSchema = external_exports.object({
|
|
|
29321
29428
|
email: emailSchema.optional(),
|
|
29322
29429
|
phone: phoneSchema.nullable().optional(),
|
|
29323
29430
|
roles: rolesSchema.optional(),
|
|
29324
|
-
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()
|
|
29325
29436
|
});
|
|
29326
29437
|
var setUserPasswordRequestSchema = external_exports.object({
|
|
29327
29438
|
password: passwordSchema
|
|
@@ -29342,7 +29453,9 @@ var contactChangeConfirmSchema = external_exports.object({
|
|
|
29342
29453
|
|
|
29343
29454
|
// ../../22-packages/11-shared/31-dto/src/team.ts
|
|
29344
29455
|
var listTeamsQuerySchema = baseRequestSchema.extend({
|
|
29345
|
-
pinned: pinnedFilterSchema
|
|
29456
|
+
pinned: pinnedFilterSchema,
|
|
29457
|
+
/** ADMIN-only Trash: list soft-deleted teams (ADR-0109). */
|
|
29458
|
+
deleted: deletedFilterSchema
|
|
29346
29459
|
});
|
|
29347
29460
|
var createTeamRequestSchema = external_exports.object({
|
|
29348
29461
|
name: external_exports.string().min(1).max(100),
|
|
@@ -29358,9 +29471,14 @@ var attachTeamUserRequestSchema = external_exports.object({
|
|
|
29358
29471
|
isLead: external_exports.boolean().optional()
|
|
29359
29472
|
});
|
|
29360
29473
|
|
|
29474
|
+
// ../../22-packages/11-shared/31-dto/src/template.ts
|
|
29475
|
+
var TEMPLATE_KINDS = ["report", "est", "wbs", "proposal", "feasibility"];
|
|
29476
|
+
|
|
29361
29477
|
// ../../22-packages/11-shared/31-dto/src/project.ts
|
|
29362
29478
|
var listProjectsQuerySchema = baseRequestSchema.extend({
|
|
29363
|
-
pinned: pinnedFilterSchema
|
|
29479
|
+
pinned: pinnedFilterSchema,
|
|
29480
|
+
/** ADMIN-only Trash: list soft-deleted projects (ADR-0109). */
|
|
29481
|
+
deleted: deletedFilterSchema
|
|
29364
29482
|
});
|
|
29365
29483
|
var recentProjectsQuerySchema = external_exports.object({
|
|
29366
29484
|
limit: external_exports.coerce.number().int().min(1).max(20).default(5)
|
|
@@ -29370,11 +29488,37 @@ var createProjectRequestSchema = external_exports.object({
|
|
|
29370
29488
|
description: external_exports.string().max(2e3).optional(),
|
|
29371
29489
|
machineUserId: external_exports.string().uuid().optional()
|
|
29372
29490
|
});
|
|
29491
|
+
var DEFAULT_OUTBOUND_RULES = {
|
|
29492
|
+
secret: [
|
|
29493
|
+
String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
|
|
29494
|
+
String.raw`/AKIA[0-9A-Z]{16}/`,
|
|
29495
|
+
String.raw`/\b(gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9]{20,})\b/`,
|
|
29496
|
+
String.raw`/-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/`,
|
|
29497
|
+
String.raw`/\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S{6,}/`
|
|
29498
|
+
],
|
|
29499
|
+
environment: [
|
|
29500
|
+
String.raw`/\b(?:ssh|scp|rsync)\s+[\w.-]+@/i`,
|
|
29501
|
+
String.raw`/\bsudo\b/i`,
|
|
29502
|
+
String.raw`/\bkubectl\b/i`,
|
|
29503
|
+
String.raw`/\bdocker\s+(?:push|-H|--host)\b/i`,
|
|
29504
|
+
String.raw`/\b(?:aws|gcloud|az)\s+\w+/i`,
|
|
29505
|
+
String.raw`/\bcurl\b[^\n]*\bhttps?:\/\/(?!localhost|127\.0\.0\.1)/i`
|
|
29506
|
+
]
|
|
29507
|
+
};
|
|
29508
|
+
function compileRulePattern(entry) {
|
|
29509
|
+
const literal = /^\/(.+)\/([gimsuy]*)$/s.exec(entry.trim());
|
|
29510
|
+
if (literal) return new RegExp(literal[1] ?? "", literal[2] ?? "");
|
|
29511
|
+
return new RegExp(entry.trim());
|
|
29512
|
+
}
|
|
29373
29513
|
var updateProjectRequestSchema = external_exports.object({
|
|
29374
29514
|
name: external_exports.string().min(1).max(100).optional(),
|
|
29375
29515
|
description: external_exports.string().max(2e3).optional(),
|
|
29376
29516
|
// Mid-tier IP allowlist (ADR-0050) — PM(manage)/ADMIN only; must fall within org.
|
|
29377
|
-
ipAllowlist: ipAllowlistSchema.optional()
|
|
29517
|
+
ipAllowlist: ipAllowlistSchema.optional(),
|
|
29518
|
+
// Messenger retention in days (0 = keep forever) — manager only (ADR-0078).
|
|
29519
|
+
messengerRetentionDays: external_exports.number().int().min(0).max(3650).optional(),
|
|
29520
|
+
// Project settings (ADR-0081) — key-level shallow-merge; `tokens` clamped on read.
|
|
29521
|
+
settings: external_exports.record(external_exports.unknown()).optional()
|
|
29378
29522
|
});
|
|
29379
29523
|
var MEMO_MAX_LENGTH = 65536;
|
|
29380
29524
|
var putMemoRequestSchema = external_exports.object({
|
|
@@ -29390,9 +29534,26 @@ var updateMemoItemSchema = external_exports.object({
|
|
|
29390
29534
|
}).refine((v) => v.content !== void 0 || v.done !== void 0, {
|
|
29391
29535
|
message: "content or done required"
|
|
29392
29536
|
});
|
|
29537
|
+
var putSpecDraftRequestSchema = external_exports.object({
|
|
29538
|
+
spec: external_exports.record(external_exports.unknown()),
|
|
29539
|
+
specVersion: external_exports.number().int().positive().optional()
|
|
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
|
+
});
|
|
29393
29552
|
var repoSpecSchema = external_exports.object({
|
|
29394
29553
|
/** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
|
|
29395
29554
|
role: external_exports.string().max(60).optional().default(""),
|
|
29555
|
+
/** Free-form description of what this repo holds (optional). */
|
|
29556
|
+
desc: external_exports.string().max(500).optional().default(""),
|
|
29396
29557
|
/** Exactly one repo of the project is the primary. */
|
|
29397
29558
|
primary: external_exports.boolean().optional().default(false),
|
|
29398
29559
|
/** `create` a new repo (provider) or use an `existing` one (url). */
|
|
@@ -29404,14 +29565,44 @@ var repoSpecSchema = external_exports.object({
|
|
|
29404
29565
|
/** Existing repo url to clone/link (op=existing). */
|
|
29405
29566
|
url: external_exports.string().max(500).optional().default(""),
|
|
29406
29567
|
/** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
|
|
29407
|
-
subdir: external_exports.string().max(120).optional().default("")
|
|
29568
|
+
subdir: external_exports.string().max(120).optional().default(""),
|
|
29569
|
+
/** Default branch for this repo (empty ⇒ provider default, e.g. `main`). */
|
|
29570
|
+
defaultBranch: external_exports.string().max(100).optional().default(""),
|
|
29571
|
+
/** `.gitignore` template name for this repo (empty ⇒ none). */
|
|
29572
|
+
gitignore: external_exports.string().max(40).optional().default(""),
|
|
29573
|
+
/** Commit-message convention for this repo (empty ⇒ unspecified). */
|
|
29574
|
+
commitConvention: external_exports.string().max(40).optional().default("")
|
|
29575
|
+
});
|
|
29576
|
+
var specFieldEnvelopeSchema = external_exports.object({
|
|
29577
|
+
id: external_exports.string().min(1),
|
|
29578
|
+
name: external_exports.string().optional().default(""),
|
|
29579
|
+
description: external_exports.string().optional().default(""),
|
|
29580
|
+
type: external_exports.string().optional().default("text"),
|
|
29581
|
+
options: external_exports.array(external_exports.string()).optional(),
|
|
29582
|
+
value: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().default("")
|
|
29583
|
+
});
|
|
29584
|
+
var subagentSpecSchema = external_exports.object({
|
|
29585
|
+
name: external_exports.string().max(120).optional().default(""),
|
|
29586
|
+
description: external_exports.string().max(2e3).optional().default("")
|
|
29408
29587
|
});
|
|
29409
29588
|
var projectSpecSchema = external_exports.object({
|
|
29410
29589
|
specVersion: external_exports.number().int().positive(),
|
|
29411
|
-
|
|
29412
|
-
|
|
29413
|
-
|
|
29414
|
-
|
|
29590
|
+
fields: external_exports.array(specFieldEnvelopeSchema),
|
|
29591
|
+
repos: external_exports.array(repoSpecSchema).optional().default([]),
|
|
29592
|
+
subagents: external_exports.array(subagentSpecSchema).optional().default([]),
|
|
29593
|
+
meta: external_exports.record(external_exports.unknown()).optional()
|
|
29594
|
+
}).superRefine((v, ctx) => {
|
|
29595
|
+
const byId = new Map(v.fields.map((f) => [f.id, f.value]));
|
|
29596
|
+
for (const key of ["id", "name"]) {
|
|
29597
|
+
const val = byId.get(key);
|
|
29598
|
+
if (typeof val !== "string" || val.trim() === "") {
|
|
29599
|
+
ctx.addIssue({
|
|
29600
|
+
code: external_exports.ZodIssueCode.custom,
|
|
29601
|
+
path: ["fields"],
|
|
29602
|
+
message: `the "${key}" field is required`
|
|
29603
|
+
});
|
|
29604
|
+
}
|
|
29605
|
+
}
|
|
29415
29606
|
if (v.repos && v.repos.length > 0) {
|
|
29416
29607
|
const primaries = v.repos.filter((r) => r.primary).length;
|
|
29417
29608
|
if (primaries !== 1) {
|
|
@@ -29425,28 +29616,39 @@ var projectSpecSchema = external_exports.object({
|
|
|
29425
29616
|
});
|
|
29426
29617
|
var createFromSpecRequestSchema = external_exports.object({
|
|
29427
29618
|
projectId: external_exports.string().uuid(),
|
|
29428
|
-
/** Target directory on the worker (from FsPicker — machine-0007). */
|
|
29429
|
-
path: external_exports.string().min(1).max(500),
|
|
29430
29619
|
spec: projectSpecSchema
|
|
29431
29620
|
});
|
|
29432
29621
|
var addRepoRequestSchema = external_exports.object({
|
|
29433
29622
|
projectId: external_exports.string().uuid(),
|
|
29434
|
-
|
|
29435
|
-
|
|
29436
|
-
|
|
29437
|
-
|
|
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
|
+
}
|
|
29438
29643
|
});
|
|
29439
|
-
|
|
29440
|
-
|
|
29441
|
-
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
spec: external_exports.record(external_exports.unknown())
|
|
29644
|
+
|
|
29645
|
+
// ../../22-packages/11-shared/31-dto/src/activity.ts
|
|
29646
|
+
var listActivityQuerySchema = baseRequestSchema.extend({
|
|
29647
|
+
/** Filter by action prefix, e.g. "project." */
|
|
29648
|
+
action: external_exports.string().max(50).optional()
|
|
29445
29649
|
});
|
|
29446
|
-
var
|
|
29447
|
-
|
|
29448
|
-
machineLinkId: external_exports.string().uuid(),
|
|
29449
|
-
spec: external_exports.record(external_exports.unknown())
|
|
29650
|
+
var lifecycleActionRequestSchema = external_exports.object({
|
|
29651
|
+
note: external_exports.string().max(500).optional()
|
|
29450
29652
|
});
|
|
29451
29653
|
|
|
29452
29654
|
// ../../22-packages/11-shared/31-dto/src/permission.ts
|
|
@@ -29505,17 +29707,43 @@ var updateMachineLinkRequestSchema = external_exports.object({
|
|
|
29505
29707
|
});
|
|
29506
29708
|
|
|
29507
29709
|
// ../../22-packages/11-shared/31-dto/src/command.ts
|
|
29710
|
+
var COMMAND_MAX_LEN = 8e3;
|
|
29711
|
+
var AI_PROMPT_MAX_LEN = 2e5;
|
|
29508
29712
|
var dispatchCommandRequestSchema = external_exports.object({
|
|
29509
29713
|
projectId: external_exports.string().uuid(),
|
|
29510
29714
|
machineLinkId: external_exports.string().uuid(),
|
|
29511
|
-
// In AI mode `command` carries the raw prompt (can be long
|
|
29512
|
-
|
|
29715
|
+
// In AI mode `command` carries the raw prompt (can be long — the spec-assist prompts
|
|
29716
|
+
// embed the whole self-describing spec envelope, ADR-0106), else an executable name.
|
|
29717
|
+
command: external_exports.string().min(1).max(AI_PROMPT_MAX_LEN),
|
|
29513
29718
|
args: external_exports.array(external_exports.string().max(1e3)).max(50).optional(),
|
|
29514
29719
|
/**
|
|
29515
29720
|
* AI prompt mode: run `command` as a prompt through the cli's AI CLI (profile
|
|
29516
29721
|
* failover) instead of spawning it verbatim (mirrors a locally-typed prompt).
|
|
29517
29722
|
*/
|
|
29518
|
-
ai: external_exports.boolean().optional()
|
|
29723
|
+
ai: external_exports.boolean().optional(),
|
|
29724
|
+
/**
|
|
29725
|
+
* Short human label shown to OTHER tabs watching this cli's activity feed (ADR-0101):
|
|
29726
|
+
* e.g. "AI review" so the Console shows a friendly line instead of the raw prompt.
|
|
29727
|
+
*/
|
|
29728
|
+
label: external_exports.string().max(120).optional()
|
|
29729
|
+
}).superRefine((v, ctx) => {
|
|
29730
|
+
if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
|
|
29731
|
+
ctx.addIssue({
|
|
29732
|
+
code: external_exports.ZodIssueCode.too_big,
|
|
29733
|
+
maximum: COMMAND_MAX_LEN,
|
|
29734
|
+
type: "string",
|
|
29735
|
+
inclusive: true,
|
|
29736
|
+
path: ["command"],
|
|
29737
|
+
message: `command too long (max ${COMMAND_MAX_LEN})`
|
|
29738
|
+
});
|
|
29739
|
+
}
|
|
29740
|
+
});
|
|
29741
|
+
var listCommandsQuerySchema = baseRequestSchema.extend({
|
|
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()
|
|
29519
29747
|
});
|
|
29520
29748
|
|
|
29521
29749
|
// ../../22-packages/11-shared/31-dto/src/invitation.ts
|
|
@@ -29603,16 +29831,68 @@ var planLimitSchema = external_exports.object({
|
|
|
29603
29831
|
period: external_exports.enum([QuotaPeriod.DAY, QuotaPeriod.MONTH]),
|
|
29604
29832
|
onExceed: external_exports.enum([QuotaOnExceed.BLOCK, QuotaOnExceed.WARN]).default(QuotaOnExceed.BLOCK)
|
|
29605
29833
|
});
|
|
29834
|
+
var planEntitlementsSchema = external_exports.object({
|
|
29835
|
+
/** Max human seats (MACHINE excluded — ADR-0041); null = unlimited. */
|
|
29836
|
+
maxUsers: external_exports.number().int().min(1).nullable().default(null),
|
|
29837
|
+
/** Max projects; null = unlimited. */
|
|
29838
|
+
maxProjects: external_exports.number().int().min(1).nullable().default(null),
|
|
29839
|
+
/** Allowed to create teams. */
|
|
29840
|
+
teams: external_exports.boolean().default(false),
|
|
29841
|
+
/** Allowed to use Community/4rum. */
|
|
29842
|
+
community: external_exports.boolean().default(false),
|
|
29843
|
+
/** Retention cap (days) for log/command/chat; null = unlimited. */
|
|
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)
|
|
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
|
+
};
|
|
29871
|
+
var checkoutRequestSchema = external_exports.object({
|
|
29872
|
+
planCode: external_exports.string().min(1).max(50),
|
|
29873
|
+
provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
|
|
29874
|
+
});
|
|
29606
29875
|
var upsertPlanRequestSchema = external_exports.object({
|
|
29607
29876
|
name: external_exports.string().min(1).max(100),
|
|
29608
29877
|
price: external_exports.number().int().min(0).default(0),
|
|
29609
|
-
limits: external_exports.array(planLimitSchema).default([])
|
|
29878
|
+
limits: external_exports.array(planLimitSchema).default([]),
|
|
29879
|
+
entitlements: planEntitlementsSchema.optional()
|
|
29610
29880
|
});
|
|
29611
29881
|
var assignPlanRequestSchema = external_exports.object({
|
|
29612
29882
|
planCode: external_exports.string().min(1).max(50),
|
|
29613
29883
|
/** Cycle length in days (defaults to 30). */
|
|
29614
29884
|
periodDays: external_exports.number().int().min(1).max(366).default(30)
|
|
29615
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
|
+
});
|
|
29616
29896
|
|
|
29617
29897
|
// ../../22-packages/11-shared/31-dto/src/search.ts
|
|
29618
29898
|
var SearchHitType = {
|
|
@@ -29626,11 +29906,11 @@ var SearchHitType = {
|
|
|
29626
29906
|
MILESTONE: "milestone",
|
|
29627
29907
|
/** Page/wiki (confluence + project docs — community). */
|
|
29628
29908
|
PAGE: "page",
|
|
29629
|
-
/**
|
|
29909
|
+
/** A message in a channel (Teams channel — community). */
|
|
29630
29910
|
MESSAGE: "message",
|
|
29631
|
-
/**
|
|
29911
|
+
/** A direct/group message (chat history — community). */
|
|
29632
29912
|
CHAT: "chat",
|
|
29633
|
-
/**
|
|
29913
|
+
/** A forum post (community news/forum). */
|
|
29634
29914
|
POST: "post"
|
|
29635
29915
|
};
|
|
29636
29916
|
var searchQuerySchema = external_exports.object({
|
|
@@ -29670,6 +29950,12 @@ var WsChannels = {
|
|
|
29670
29950
|
FS_READ: "fs.read",
|
|
29671
29951
|
GIT_DIFF: "git.diff",
|
|
29672
29952
|
GIT_ENV: "git.env",
|
|
29953
|
+
// git history browse (read-through cli — ADR-0089): repos on the physic project,
|
|
29954
|
+
// commit log, changed files of a commit, parent↔commit diff of one file.
|
|
29955
|
+
GIT_REPOS: "git.repos",
|
|
29956
|
+
GIT_LOG: "git.log",
|
|
29957
|
+
GIT_COMMIT: "git.commit",
|
|
29958
|
+
GIT_COMMIT_DIFF: "git.commit-diff",
|
|
29673
29959
|
PROJECT_CREATE: "project.create",
|
|
29674
29960
|
PROJECT_ADD: "project.add",
|
|
29675
29961
|
PROJECT_PROGRESS: "project.progress",
|
|
@@ -29681,8 +29967,8 @@ var WsChannels = {
|
|
|
29681
29967
|
PHYSIC_DELETE: "physic.delete",
|
|
29682
29968
|
// command lifecycle
|
|
29683
29969
|
COMMAND_ANNOUNCE: "command.announce",
|
|
29684
|
-
|
|
29685
|
-
|
|
29970
|
+
// AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
|
|
29971
|
+
// command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
|
|
29686
29972
|
// quota & usage (ADR-0020)
|
|
29687
29973
|
QUOTA_CHECK: "quota.check",
|
|
29688
29974
|
USAGE_REPORT: "usage.report",
|
|
@@ -29690,8 +29976,18 @@ var WsChannels = {
|
|
|
29690
29976
|
MACHINE_USAGE: "machine.usage",
|
|
29691
29977
|
// cli → server: durable per-command history record on finish (ADR-0072)
|
|
29692
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",
|
|
29693
29982
|
// server → cli (reply): tail the cli's own JSONL logs on demand (ADR-0072)
|
|
29694
|
-
LOG_READ: "log.read"
|
|
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",
|
|
29987
|
+
// outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
|
|
29988
|
+
REVIEW_REQUEST: "review.request",
|
|
29989
|
+
REVIEW_EVALUATE: "review.evaluate",
|
|
29990
|
+
REVIEW_RESULT: "review.result"
|
|
29695
29991
|
};
|
|
29696
29992
|
|
|
29697
29993
|
// ../../22-packages/21-protocol/21-websocket/src/crypto.ts
|
|
@@ -29816,7 +30112,7 @@ import { spawn } from "child_process";
|
|
|
29816
30112
|
|
|
29817
30113
|
// src/common/logger/logger.ts
|
|
29818
30114
|
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
29819
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
30115
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
|
|
29820
30116
|
import { join as join6 } from "path";
|
|
29821
30117
|
var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
29822
30118
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -29835,6 +30131,13 @@ var CliLogger = class {
|
|
|
29835
30131
|
this.retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
29836
30132
|
void this.cleanupOldLogs();
|
|
29837
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
|
+
}
|
|
29838
30141
|
/** Log an error-level event. */
|
|
29839
30142
|
error(event, fields) {
|
|
29840
30143
|
this.write("error", event, fields);
|
|
@@ -29891,6 +30194,24 @@ function readRecentLogLines(dir2, limit) {
|
|
|
29891
30194
|
return [];
|
|
29892
30195
|
}
|
|
29893
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
|
+
}
|
|
29894
30215
|
|
|
29895
30216
|
// src/core/update-scheduler.ts
|
|
29896
30217
|
var TICK_MS = 6e4;
|
|
@@ -30026,7 +30347,7 @@ async function runCommand(dispatch, emit) {
|
|
|
30026
30347
|
});
|
|
30027
30348
|
child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
|
|
30028
30349
|
child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
|
|
30029
|
-
await new Promise((
|
|
30350
|
+
await new Promise((resolve6) => {
|
|
30030
30351
|
child.on("close", (exitCode) => {
|
|
30031
30352
|
batcher.flush();
|
|
30032
30353
|
emit({
|
|
@@ -30036,7 +30357,7 @@ async function runCommand(dispatch, emit) {
|
|
|
30036
30357
|
done: true,
|
|
30037
30358
|
exitCode: exitCode ?? -1
|
|
30038
30359
|
});
|
|
30039
|
-
|
|
30360
|
+
resolve6();
|
|
30040
30361
|
});
|
|
30041
30362
|
child.on("error", (err) => {
|
|
30042
30363
|
emit({
|
|
@@ -30047,7 +30368,7 @@ async function runCommand(dispatch, emit) {
|
|
|
30047
30368
|
done: true,
|
|
30048
30369
|
exitCode: -1
|
|
30049
30370
|
});
|
|
30050
|
-
|
|
30371
|
+
resolve6();
|
|
30051
30372
|
});
|
|
30052
30373
|
});
|
|
30053
30374
|
}
|
|
@@ -30127,10 +30448,10 @@ function isSessionLimit(text) {
|
|
|
30127
30448
|
);
|
|
30128
30449
|
}
|
|
30129
30450
|
function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
|
|
30130
|
-
return new Promise((
|
|
30451
|
+
return new Promise((resolve6) => {
|
|
30131
30452
|
void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
|
|
30132
30453
|
if (out.chunk) onChunk(out.chunk);
|
|
30133
|
-
if (out.done)
|
|
30454
|
+
if (out.done) resolve6(out.exitCode ?? -1);
|
|
30134
30455
|
});
|
|
30135
30456
|
});
|
|
30136
30457
|
}
|
|
@@ -30149,7 +30470,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
|
30149
30470
|
handlers.onChunk(text);
|
|
30150
30471
|
}
|
|
30151
30472
|
};
|
|
30152
|
-
finalExit = await runAttempt(commandId, plan.cmd,
|
|
30473
|
+
finalExit = await runAttempt(commandId, plan.cmd, attempt.args, cwd2, attempt.env, emit);
|
|
30153
30474
|
const tail = parser.flush();
|
|
30154
30475
|
if (tail) {
|
|
30155
30476
|
captured += tail;
|
|
@@ -30209,13 +30530,18 @@ function resolveHomePath(value) {
|
|
|
30209
30530
|
if (isAbsolute(value)) return value;
|
|
30210
30531
|
return join8(homedir3(), value);
|
|
30211
30532
|
}
|
|
30212
|
-
function
|
|
30213
|
-
if (!
|
|
30214
|
-
|
|
30215
|
-
|
|
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];
|
|
30216
30542
|
}
|
|
30217
|
-
function
|
|
30218
|
-
const dirs =
|
|
30543
|
+
function claudeHomeDirs(config) {
|
|
30544
|
+
const dirs = profileDirs(config.claudeHome);
|
|
30219
30545
|
return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
|
|
30220
30546
|
}
|
|
30221
30547
|
function profileDisplayLabel(dir2) {
|
|
@@ -30232,28 +30558,40 @@ function profileEnvVar(cmd) {
|
|
|
30232
30558
|
if (cmd.includes("codex")) return "CODEX_HOME";
|
|
30233
30559
|
return null;
|
|
30234
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
|
+
}
|
|
30235
30567
|
function profileLabels(config) {
|
|
30236
30568
|
const cmd = config.aiCli ?? DEFAULT_AI_CLI;
|
|
30237
|
-
|
|
30238
|
-
const dirs = envVar === "CLAUDE_CONFIG_DIR" ? resolveDirs(config.claudeConfigDir) : envVar === "CODEX_HOME" ? resolveDirs(config.codexHome) : [];
|
|
30239
|
-
return dirs.map((dir2) => profileDisplayLabel(dir2));
|
|
30569
|
+
return profileDirs(profilesFor(config, cmd)).map((dir2) => profileDisplayLabel(dir2));
|
|
30240
30570
|
}
|
|
30241
30571
|
function planAiRun(prompt, config, workingDir) {
|
|
30242
30572
|
const cmd = config.aiCli ?? DEFAULT_AI_CLI;
|
|
30243
|
-
const args = [...config.aiArgs ?? DEFAULT_AI_ARGS[cmd] ?? [], prompt];
|
|
30244
30573
|
const baseEnv = config.aiEnv ?? {};
|
|
30245
30574
|
const envVar = profileEnvVar(cmd);
|
|
30246
|
-
const
|
|
30247
|
-
|
|
30248
|
-
|
|
30249
|
-
|
|
30250
|
-
|
|
30251
|
-
|
|
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 }) => ({
|
|
30252
30589
|
label: profileDisplayLabel(dir2),
|
|
30253
30590
|
dir: dir2,
|
|
30591
|
+
args: buildRunArgs(profile, cmd, prompt),
|
|
30254
30592
|
env: { ...baseEnv, [envVar]: dir2 }
|
|
30255
30593
|
}));
|
|
30256
|
-
return { cmd,
|
|
30594
|
+
return { cmd, attempts };
|
|
30257
30595
|
}
|
|
30258
30596
|
|
|
30259
30597
|
// src/core/claude-usage.ts
|
|
@@ -30291,16 +30629,16 @@ async function fetchUsage(token) {
|
|
|
30291
30629
|
}
|
|
30292
30630
|
}
|
|
30293
30631
|
function runUsageCli(dir2, aiCli2) {
|
|
30294
|
-
return new Promise((
|
|
30632
|
+
return new Promise((resolve6) => {
|
|
30295
30633
|
let out = "";
|
|
30296
30634
|
let child = null;
|
|
30297
30635
|
const finish = () => {
|
|
30298
30636
|
clearTimeout(timer);
|
|
30299
|
-
|
|
30637
|
+
resolve6(out);
|
|
30300
30638
|
};
|
|
30301
30639
|
const timer = setTimeout(() => {
|
|
30302
30640
|
child?.kill();
|
|
30303
|
-
|
|
30641
|
+
resolve6(out);
|
|
30304
30642
|
}, 3e4);
|
|
30305
30643
|
timer.unref?.();
|
|
30306
30644
|
try {
|
|
@@ -30423,26 +30761,95 @@ async function runAi(prompt) {
|
|
|
30423
30761
|
});
|
|
30424
30762
|
return parseAi(cli, stdout, prompt);
|
|
30425
30763
|
}
|
|
30426
|
-
async function
|
|
30764
|
+
async function aiGenerate(prompt) {
|
|
30765
|
+
const { text } = await runAi(prompt);
|
|
30766
|
+
return text;
|
|
30767
|
+
}
|
|
30768
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
30769
|
+
const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
|
|
30770
|
+
${input}`;
|
|
30771
|
+
const { text, tokens } = await runAi(prompt);
|
|
30772
|
+
const start = text.indexOf("{");
|
|
30773
|
+
const end = text.lastIndexOf("}");
|
|
30774
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
30427
30775
|
try {
|
|
30428
|
-
const
|
|
30429
|
-
|
|
30430
|
-
|
|
30431
|
-
|
|
30432
|
-
|
|
30433
|
-
return { value: "", error: err instanceof Error ? err.message : String(err) };
|
|
30776
|
+
const j = JSON.parse(text.slice(start, end + 1));
|
|
30777
|
+
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
30778
|
+
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
30779
|
+
} catch {
|
|
30780
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
30434
30781
|
}
|
|
30435
30782
|
}
|
|
30436
|
-
|
|
30437
|
-
|
|
30438
|
-
|
|
30439
|
-
|
|
30440
|
-
|
|
30441
|
-
|
|
30442
|
-
|
|
30443
|
-
|
|
30444
|
-
|
|
30783
|
+
|
|
30784
|
+
// src/core/outbound-review.ts
|
|
30785
|
+
function compilePatterns(sources, fallback) {
|
|
30786
|
+
const list = sources ?? fallback;
|
|
30787
|
+
const out = [];
|
|
30788
|
+
for (const src of list) {
|
|
30789
|
+
try {
|
|
30790
|
+
out.push(compileRulePattern(src));
|
|
30791
|
+
} catch (err) {
|
|
30792
|
+
logger.warn("outbound-review.bad-pattern", {
|
|
30793
|
+
pattern: src,
|
|
30794
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30795
|
+
});
|
|
30796
|
+
}
|
|
30797
|
+
}
|
|
30798
|
+
return out;
|
|
30799
|
+
}
|
|
30800
|
+
function referencedRepos(text) {
|
|
30801
|
+
const repos = /* @__PURE__ */ new Set();
|
|
30802
|
+
const re = /(?:https?:\/\/|git@)([\w.-]+)[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?(?=[\s"')]|$)/gi;
|
|
30803
|
+
let m;
|
|
30804
|
+
while (m = re.exec(text)) repos.add(`${m[1]}/${m[2]}`.toLowerCase());
|
|
30805
|
+
return [...repos];
|
|
30806
|
+
}
|
|
30807
|
+
function ruleReview(input, allowedRepos, projectPath, secretPatterns, envPatterns) {
|
|
30808
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
30809
|
+
const secretRe = compilePatterns(secretPatterns, DEFAULT_OUTBOUND_RULES.secret);
|
|
30810
|
+
const envRe = compilePatterns(envPatterns, DEFAULT_OUTBOUND_RULES.environment);
|
|
30811
|
+
if (secretRe.some((re) => re.test(input))) reasons.add("secret");
|
|
30812
|
+
if (envRe.some((re) => re.test(input))) reasons.add("environment");
|
|
30813
|
+
if (/(?:^|[\s"'(])(?:\.\.[/\\])+/.test(input)) reasons.add("path");
|
|
30814
|
+
const absSystem = /(?:^|[\s"'(=])(\/(?:etc|root|var|usr|bin|home|private|proc|sys)\b|[A-Za-z]:\\\\)/;
|
|
30815
|
+
if (absSystem.test(input) && projectPath && !input.includes(projectPath)) reasons.add("path");
|
|
30816
|
+
if (allowedRepos.length) {
|
|
30817
|
+
const allow = allowedRepos.map((r) => r.toLowerCase());
|
|
30818
|
+
const refs = referencedRepos(input);
|
|
30819
|
+
if (refs.some((r) => !allow.some((a) => a.includes(r) || r.includes(a)))) reasons.add("repo");
|
|
30820
|
+
}
|
|
30821
|
+
return [...reasons];
|
|
30822
|
+
}
|
|
30823
|
+
async function evaluateReview(payload) {
|
|
30824
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
30825
|
+
let tokens = 0;
|
|
30826
|
+
if (payload.ruleCheck) {
|
|
30827
|
+
const violations = ruleReview(
|
|
30828
|
+
payload.prompt,
|
|
30829
|
+
payload.allowedRepos,
|
|
30830
|
+
payload.projectPath ?? "",
|
|
30831
|
+
payload.secretPatterns,
|
|
30832
|
+
payload.envPatterns
|
|
30833
|
+
);
|
|
30834
|
+
for (const r of violations) reasons.add(r);
|
|
30835
|
+
}
|
|
30836
|
+
if (payload.aiReview) {
|
|
30837
|
+
try {
|
|
30838
|
+
const ai = await aiOutboundReview(
|
|
30839
|
+
payload.prompt,
|
|
30840
|
+
payload.allowedRepos,
|
|
30841
|
+
payload.projectPath ?? ""
|
|
30842
|
+
);
|
|
30843
|
+
tokens = ai.tokens;
|
|
30844
|
+
if (!ai.ok) for (const r of ai.reasons) reasons.add(r);
|
|
30845
|
+
} catch (err) {
|
|
30846
|
+
logger.warn("outbound-review.ai-error", {
|
|
30847
|
+
commandId: payload.commandId,
|
|
30848
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30849
|
+
});
|
|
30850
|
+
}
|
|
30445
30851
|
}
|
|
30852
|
+
return { commandId: payload.commandId, ok: reasons.size === 0, reasons: [...reasons], tokens };
|
|
30446
30853
|
}
|
|
30447
30854
|
|
|
30448
30855
|
// src/core/command-history.ts
|
|
@@ -30539,6 +30946,13 @@ function recordCommand(entry) {
|
|
|
30539
30946
|
function getCommandHistory() {
|
|
30540
30947
|
return entries;
|
|
30541
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
|
+
}
|
|
30542
30956
|
function finishCommand(commandId, exitCode, usage) {
|
|
30543
30957
|
const entry = entries.find((e) => e.commandId === commandId && e.status === "running");
|
|
30544
30958
|
if (!entry) return;
|
|
@@ -30566,7 +30980,7 @@ import {
|
|
|
30566
30980
|
readFileSync as readFileSync11,
|
|
30567
30981
|
readdirSync as readdirSync3,
|
|
30568
30982
|
rmSync as rmSync3,
|
|
30569
|
-
statSync
|
|
30983
|
+
statSync as statSync2
|
|
30570
30984
|
} from "fs";
|
|
30571
30985
|
import { join as join11 } from "path";
|
|
30572
30986
|
var KEEP_FILES = 200;
|
|
@@ -30599,11 +31013,22 @@ function readCommandOutput(commandId) {
|
|
|
30599
31013
|
function prune() {
|
|
30600
31014
|
if (!dir) return;
|
|
30601
31015
|
try {
|
|
30602
|
-
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);
|
|
30603
31017
|
for (const { f } of files.slice(KEEP_FILES)) rmSync3(join11(dir, f), { force: true });
|
|
30604
31018
|
} catch {
|
|
30605
31019
|
}
|
|
30606
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
|
+
}
|
|
30607
31032
|
|
|
30608
31033
|
// src/core/fs-browse.ts
|
|
30609
31034
|
import { readdir as readdir2 } from "fs/promises";
|
|
@@ -30703,13 +31128,142 @@ ${stderr}`;
|
|
|
30703
31128
|
return { installed, authenticated, account, claudeCli };
|
|
30704
31129
|
}
|
|
30705
31130
|
|
|
30706
|
-
// src/core/
|
|
30707
|
-
import {
|
|
31131
|
+
// src/core/git-history.ts
|
|
31132
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
30708
31133
|
import { execFile as execFile4 } from "child_process";
|
|
30709
|
-
import {
|
|
30710
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
31134
|
+
import { resolve as resolve4, sep as sep3 } from "path";
|
|
30711
31135
|
import { promisify as promisify4 } from "util";
|
|
30712
31136
|
var run4 = promisify4(execFile4);
|
|
31137
|
+
var FS = "";
|
|
31138
|
+
var RS = "";
|
|
31139
|
+
function resolveRepo(root, repo) {
|
|
31140
|
+
if (!root) return null;
|
|
31141
|
+
const base = resolve4(root);
|
|
31142
|
+
const requested = resolve4(base, repo && repo.trim() ? repo : ".");
|
|
31143
|
+
return requested === base || requested.startsWith(base + sep3) ? requested : base;
|
|
31144
|
+
}
|
|
31145
|
+
async function git(cwd2, args) {
|
|
31146
|
+
try {
|
|
31147
|
+
const { stdout } = await run4("git", args, {
|
|
31148
|
+
cwd: cwd2,
|
|
31149
|
+
timeout: 15e3,
|
|
31150
|
+
maxBuffer: 8 * 1024 * 1024
|
|
31151
|
+
});
|
|
31152
|
+
return stdout;
|
|
31153
|
+
} catch {
|
|
31154
|
+
return "";
|
|
31155
|
+
}
|
|
31156
|
+
}
|
|
31157
|
+
async function gitRepos(root) {
|
|
31158
|
+
if (!root) return { repos: [] };
|
|
31159
|
+
const base = resolve4(root);
|
|
31160
|
+
const found = /* @__PURE__ */ new Set();
|
|
31161
|
+
const repos = [];
|
|
31162
|
+
const add = async (dir2) => {
|
|
31163
|
+
const inside = await git(dir2, ["rev-parse", "--show-toplevel"]);
|
|
31164
|
+
const top = inside.trim();
|
|
31165
|
+
if (!top || found.has(top) || !(top === base || top.startsWith(base + sep3))) return;
|
|
31166
|
+
found.add(top);
|
|
31167
|
+
const subdir = top === base ? "" : top.slice(base.length + 1);
|
|
31168
|
+
const remote = (await git(top, ["remote", "get-url", "origin"])).trim();
|
|
31169
|
+
repos.push({ subdir, name: subdir || "", remote: remote || null });
|
|
31170
|
+
};
|
|
31171
|
+
await add(base);
|
|
31172
|
+
try {
|
|
31173
|
+
const lvl1 = await readdir3(base, { withFileTypes: true });
|
|
31174
|
+
for (const d of lvl1) {
|
|
31175
|
+
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
31176
|
+
const p1 = resolve4(base, d.name);
|
|
31177
|
+
await add(p1);
|
|
31178
|
+
try {
|
|
31179
|
+
const lvl2 = await readdir3(p1, { withFileTypes: true });
|
|
31180
|
+
for (const d2 of lvl2) {
|
|
31181
|
+
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
31182
|
+
await add(resolve4(p1, d2.name));
|
|
31183
|
+
}
|
|
31184
|
+
} catch {
|
|
31185
|
+
}
|
|
31186
|
+
}
|
|
31187
|
+
} catch {
|
|
31188
|
+
}
|
|
31189
|
+
repos.sort((a, b) => a.subdir.localeCompare(b.subdir));
|
|
31190
|
+
return { repos };
|
|
31191
|
+
}
|
|
31192
|
+
async function gitLog(root, repo, skip, limit) {
|
|
31193
|
+
const cwd2 = resolveRepo(root, repo);
|
|
31194
|
+
if (!cwd2) return { entries: [], hasMore: false };
|
|
31195
|
+
const capped = Math.min(Math.max(limit, 1), 200);
|
|
31196
|
+
const format = ["%H", "%h", "%an", "%aI", "%s"].join(FS) + RS;
|
|
31197
|
+
const stdout = await git(cwd2, [
|
|
31198
|
+
"log",
|
|
31199
|
+
`--skip=${Math.max(skip, 0)}`,
|
|
31200
|
+
`-n`,
|
|
31201
|
+
String(capped + 1),
|
|
31202
|
+
`--pretty=format:${format}`
|
|
31203
|
+
]);
|
|
31204
|
+
const rows = stdout.split(RS).map((r) => r.replace(/^\n/, "")).filter((r) => r.length > 0);
|
|
31205
|
+
const entries2 = rows.slice(0, capped).map((row) => {
|
|
31206
|
+
const [hash = "", shortHash = "", author = "", date = "", subject = ""] = row.split(FS);
|
|
31207
|
+
return { hash, shortHash, author, date, subject };
|
|
31208
|
+
});
|
|
31209
|
+
return { entries: entries2, hasMore: rows.length > capped };
|
|
31210
|
+
}
|
|
31211
|
+
async function gitCommit(root, repo, hash) {
|
|
31212
|
+
const cwd2 = resolveRepo(root, repo);
|
|
31213
|
+
const empty = { hash, author: "", date: "", subject: "", files: [] };
|
|
31214
|
+
if (!cwd2 || !hash) return empty;
|
|
31215
|
+
const format = ["%H", "%an", "%aI", "%s"].join(FS);
|
|
31216
|
+
const stdout = await git(cwd2, [
|
|
31217
|
+
"show",
|
|
31218
|
+
"--name-status",
|
|
31219
|
+
"-M",
|
|
31220
|
+
`--pretty=format:${format}`,
|
|
31221
|
+
hash
|
|
31222
|
+
]);
|
|
31223
|
+
if (!stdout) return empty;
|
|
31224
|
+
const lines = stdout.split("\n");
|
|
31225
|
+
const [full = hash, author = "", date = "", subject = ""] = (lines[0] ?? "").split(FS);
|
|
31226
|
+
const files = [];
|
|
31227
|
+
for (const line of lines.slice(1)) {
|
|
31228
|
+
if (!line.trim()) continue;
|
|
31229
|
+
const parts = line.split(" ");
|
|
31230
|
+
const status = (parts[0] ?? "").charAt(0);
|
|
31231
|
+
if (!status) continue;
|
|
31232
|
+
if (status === "R" || status === "C") {
|
|
31233
|
+
files.push({ path: parts[2] ?? "", status, oldPath: parts[1] ?? void 0 });
|
|
31234
|
+
} else {
|
|
31235
|
+
files.push({ path: parts[1] ?? "", status });
|
|
31236
|
+
}
|
|
31237
|
+
}
|
|
31238
|
+
return { hash: full, author, date, subject, files };
|
|
31239
|
+
}
|
|
31240
|
+
async function gitCommitDiff(root, repo, hash, path) {
|
|
31241
|
+
const cwd2 = resolveRepo(root, repo);
|
|
31242
|
+
if (!cwd2 || !hash || !path) return { path, oldContent: "", newContent: "" };
|
|
31243
|
+
const oldContent = await git(cwd2, ["show", `${hash}^:${path}`]);
|
|
31244
|
+
const newContent = await git(cwd2, ["show", `${hash}:${path}`]);
|
|
31245
|
+
return { path, oldContent, newContent };
|
|
31246
|
+
}
|
|
31247
|
+
|
|
31248
|
+
// src/core/git-commit-identity.ts
|
|
31249
|
+
import { execFile as execFile5 } from "child_process";
|
|
31250
|
+
import { promisify as promisify5 } from "util";
|
|
31251
|
+
var run5 = promisify5(execFile5);
|
|
31252
|
+
async function setCommitAuthor(cwd2, machineUsername) {
|
|
31253
|
+
if (!machineUsername) return;
|
|
31254
|
+
try {
|
|
31255
|
+
await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
31256
|
+
} catch {
|
|
31257
|
+
}
|
|
31258
|
+
}
|
|
31259
|
+
|
|
31260
|
+
// src/core/scaffold.ts
|
|
31261
|
+
import { cp, mkdir as mkdir2, writeFile } from "fs/promises";
|
|
31262
|
+
import { execFile as execFile6 } from "child_process";
|
|
31263
|
+
import { dirname as dirname4, join as join12, resolve as resolve5 } from "path";
|
|
31264
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
31265
|
+
import { promisify as promisify6 } from "util";
|
|
31266
|
+
var run6 = promisify6(execFile6);
|
|
30713
31267
|
function reposOf(spec) {
|
|
30714
31268
|
const repos = spec?.repos;
|
|
30715
31269
|
return Array.isArray(repos) ? repos : [];
|
|
@@ -30718,13 +31272,18 @@ function subDirName(repo) {
|
|
|
30718
31272
|
const raw = (repo.subdir || repo.role || repo.name || "repo").trim();
|
|
30719
31273
|
return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
30720
31274
|
}
|
|
30721
|
-
async function provisionRepos(target, repos, emit) {
|
|
31275
|
+
async function provisionRepos(target, repos, emit, opts = {}) {
|
|
30722
31276
|
for (const repo of repos) {
|
|
30723
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
|
+
}
|
|
30724
31283
|
emit("git", "Initializing the primary repo\u2026");
|
|
30725
|
-
await
|
|
31284
|
+
await run6("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
30726
31285
|
if (repo.op === "existing" && repo.url) {
|
|
30727
|
-
await
|
|
31286
|
+
await run6("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
30728
31287
|
() => void 0
|
|
30729
31288
|
);
|
|
30730
31289
|
}
|
|
@@ -30733,23 +31292,24 @@ async function provisionRepos(target, repos, emit) {
|
|
|
30733
31292
|
const dir2 = join12(target, subDirName(repo));
|
|
30734
31293
|
if (repo.op === "existing" && repo.url) {
|
|
30735
31294
|
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
30736
|
-
await
|
|
31295
|
+
await run6("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
30737
31296
|
} else {
|
|
30738
31297
|
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
30739
31298
|
await mkdir2(dir2, { recursive: true });
|
|
30740
|
-
await
|
|
31299
|
+
await run6("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
30741
31300
|
}
|
|
30742
31301
|
}
|
|
30743
31302
|
}
|
|
30744
31303
|
function sampleDir() {
|
|
30745
31304
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
30746
31305
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
30747
|
-
return
|
|
31306
|
+
return resolve5(here, "../../project-sample");
|
|
30748
31307
|
}
|
|
30749
|
-
async function scaffoldProject(payload, onProgress) {
|
|
31308
|
+
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
30750
31309
|
const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
|
|
30751
31310
|
try {
|
|
30752
|
-
const
|
|
31311
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
|
|
31312
|
+
const target = join12(root, payload.projectName);
|
|
30753
31313
|
await mkdir2(target, { recursive: true });
|
|
30754
31314
|
emit("copy", "Copying the sample template\u2026");
|
|
30755
31315
|
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
@@ -30763,25 +31323,58 @@ async function scaffoldProject(payload, onProgress) {
|
|
|
30763
31323
|
}
|
|
30764
31324
|
const repos = reposOf(payload.spec);
|
|
30765
31325
|
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
31326
|
+
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
30766
31327
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
30767
31328
|
return { ok: true, path: target };
|
|
30768
31329
|
} catch (err) {
|
|
30769
31330
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
30770
31331
|
}
|
|
30771
31332
|
}
|
|
30772
|
-
async function
|
|
31333
|
+
async function aiInit(target, spec, emit) {
|
|
31334
|
+
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
31335
|
+
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
31336
|
+
if (subagents.length > 0) {
|
|
31337
|
+
await mkdir2(join12(target, ".claude", "agents"), { recursive: true });
|
|
31338
|
+
for (const sa of subagents) {
|
|
31339
|
+
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31340
|
+
if (!name) continue;
|
|
31341
|
+
const body = `---
|
|
31342
|
+
name: ${name}
|
|
31343
|
+
---
|
|
31344
|
+
|
|
31345
|
+
${(sa.description || "").trim()}
|
|
31346
|
+
`;
|
|
31347
|
+
await writeFile(join12(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
31348
|
+
}
|
|
31349
|
+
}
|
|
31350
|
+
const specJson = JSON.stringify(spec);
|
|
31351
|
+
await generateFile(
|
|
31352
|
+
join12(target, "README.md"),
|
|
31353
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
31354
|
+
${specJson}`
|
|
31355
|
+
);
|
|
31356
|
+
await generateFile(
|
|
31357
|
+
join12(target, "CLAUDE.md"),
|
|
31358
|
+
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
31359
|
+
${specJson}`
|
|
31360
|
+
);
|
|
31361
|
+
}
|
|
31362
|
+
async function generateFile(path, prompt) {
|
|
31363
|
+
try {
|
|
31364
|
+
const text = await aiGenerate(prompt);
|
|
31365
|
+
if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
|
|
31366
|
+
} catch {
|
|
31367
|
+
}
|
|
31368
|
+
}
|
|
31369
|
+
async function addProject(payload, profileDir2, onProgress) {
|
|
30773
31370
|
const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
|
|
30774
31371
|
try {
|
|
30775
|
-
const
|
|
31372
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
|
|
31373
|
+
const target = join12(root, payload.projectName);
|
|
30776
31374
|
await mkdir2(target, { recursive: true });
|
|
30777
|
-
|
|
30778
|
-
|
|
30779
|
-
|
|
30780
|
-
} else {
|
|
30781
|
-
emit("init", "git init\u2026");
|
|
30782
|
-
await run4("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
30783
|
-
}
|
|
30784
|
-
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 });
|
|
30785
31378
|
return { ok: true, path: target };
|
|
30786
31379
|
} catch (err) {
|
|
30787
31380
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -30791,9 +31384,10 @@ async function addProject(payload, onProgress) {
|
|
|
30791
31384
|
// src/core/ws-client.ts
|
|
30792
31385
|
var HEARTBEAT_MS = 3e4;
|
|
30793
31386
|
var USAGE_POLL_MS = 5 * 6e4;
|
|
31387
|
+
var LOG_UPLOAD_MS = 10 * 6e4;
|
|
30794
31388
|
var RECONNECT_BACKOFF_BASE_MS = 1e3;
|
|
30795
|
-
var RECONNECT_MAX_BACKOFF_DEFAULT_SEC =
|
|
30796
|
-
var RECONNECT_BACKOFF_MAX_ATTEMPT =
|
|
31389
|
+
var RECONNECT_MAX_BACKOFF_DEFAULT_SEC = 60;
|
|
31390
|
+
var RECONNECT_BACKOFF_MAX_ATTEMPT = 6;
|
|
30797
31391
|
function envReconnectMaxSec() {
|
|
30798
31392
|
const raw = Number(process.env.FOURPM_RECONNECT_MAX_BACKOFF_SEC);
|
|
30799
31393
|
if (!Number.isFinite(raw)) return null;
|
|
@@ -30818,11 +31412,20 @@ var WsClient = class {
|
|
|
30818
31412
|
context;
|
|
30819
31413
|
socket = null;
|
|
30820
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;
|
|
30821
31419
|
ecdh = null;
|
|
30822
31420
|
wsToken = "";
|
|
30823
31421
|
heartbeat = null;
|
|
30824
31422
|
usageTimer = null;
|
|
31423
|
+
logUploadTimer = null;
|
|
31424
|
+
/** Signature (fileName + totalBytes) of the last log upload — skip re-sending if unchanged. */
|
|
31425
|
+
lastLogSig = null;
|
|
30825
31426
|
usageSnapshot = null;
|
|
31427
|
+
/** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
|
|
31428
|
+
outboundReviewEnabled = false;
|
|
30826
31429
|
stopped = false;
|
|
30827
31430
|
/** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
|
|
30828
31431
|
wakeReconnect = null;
|
|
@@ -30836,11 +31439,20 @@ var WsClient = class {
|
|
|
30836
31439
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
30837
31440
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
30838
31441
|
physicRoot = null;
|
|
31442
|
+
/** This machine-user's username — the git commit author for this project (ADR-0097).
|
|
31443
|
+
* Push uses the account already logged in with gh/glab on the worker. */
|
|
31444
|
+
machineUsername = "";
|
|
30839
31445
|
/** Waiters resolved when the session next becomes connected (readiness gate before
|
|
30840
31446
|
* running a local prompt). */
|
|
30841
31447
|
connectWaiters = [];
|
|
30842
31448
|
/** Output buffered while disconnected (flushed on reconnect). */
|
|
30843
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();
|
|
30844
31456
|
offlineBufferBytes = 0;
|
|
30845
31457
|
/** cli → server requests awaiting a reply (quota.check — ADR-0020). */
|
|
30846
31458
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -30850,6 +31462,11 @@ var WsClient = class {
|
|
|
30850
31462
|
get bus() {
|
|
30851
31463
|
return this.context.bus;
|
|
30852
31464
|
}
|
|
31465
|
+
/** True once the session stopped for good (logout / replaced / version) — the
|
|
31466
|
+
* resilient run wrapper uses this to tell a clean stop from a crash to recover from. */
|
|
31467
|
+
get isStopped() {
|
|
31468
|
+
return this.stopped;
|
|
31469
|
+
}
|
|
30853
31470
|
/**
|
|
30854
31471
|
* Force an immediate reconnect: wake the backoff sleep if waiting, else drop the live
|
|
30855
31472
|
* socket so the run() loop reconnects (the next backoff is skipped). No-op if stopped.
|
|
@@ -30870,43 +31487,50 @@ var WsClient = class {
|
|
|
30870
31487
|
*/
|
|
30871
31488
|
awaitConnected(timeoutMs) {
|
|
30872
31489
|
if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
|
|
30873
|
-
return new Promise((
|
|
31490
|
+
return new Promise((resolve6) => {
|
|
30874
31491
|
const waiter = () => {
|
|
30875
31492
|
clearTimeout(timer);
|
|
30876
|
-
|
|
31493
|
+
resolve6(true);
|
|
30877
31494
|
};
|
|
30878
31495
|
const timer = setTimeout(() => {
|
|
30879
31496
|
this.connectWaiters = this.connectWaiters.filter((w) => w !== waiter);
|
|
30880
|
-
|
|
31497
|
+
resolve6(false);
|
|
30881
31498
|
}, timeoutMs);
|
|
30882
31499
|
this.connectWaiters.push(waiter);
|
|
30883
31500
|
});
|
|
30884
31501
|
}
|
|
30885
31502
|
/**
|
|
30886
31503
|
* Resolve the reconnect-backoff cap (ms): the org value (from ws_token) over the
|
|
30887
|
-
* 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.
|
|
30888
31505
|
*/
|
|
30889
31506
|
reconnectMaxMs() {
|
|
30890
31507
|
const sec = this.orgReconnectMaxSec ?? this.envReconnectMaxSec ?? RECONNECT_MAX_BACKOFF_DEFAULT_SEC;
|
|
30891
31508
|
return sec * 1e3;
|
|
30892
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
|
+
}
|
|
30893
31518
|
/** Backoff sleep that can be woken early by reconnectNow(). */
|
|
30894
31519
|
interruptibleSleep(ms) {
|
|
30895
31520
|
if (this.forceReconnect) {
|
|
30896
31521
|
this.forceReconnect = false;
|
|
30897
31522
|
return Promise.resolve();
|
|
30898
31523
|
}
|
|
30899
|
-
return new Promise((
|
|
31524
|
+
return new Promise((resolve6) => {
|
|
30900
31525
|
const timer = setTimeout(() => {
|
|
30901
31526
|
this.wakeReconnect = null;
|
|
30902
|
-
|
|
31527
|
+
resolve6();
|
|
30903
31528
|
}, ms);
|
|
30904
|
-
timer.unref?.();
|
|
30905
31529
|
this.wakeReconnect = () => {
|
|
30906
31530
|
clearTimeout(timer);
|
|
30907
31531
|
this.wakeReconnect = null;
|
|
30908
31532
|
this.forceReconnect = false;
|
|
30909
|
-
|
|
31533
|
+
resolve6();
|
|
30910
31534
|
};
|
|
30911
31535
|
});
|
|
30912
31536
|
}
|
|
@@ -30918,8 +31542,10 @@ var WsClient = class {
|
|
|
30918
31542
|
for (let attempt = 0; !this.stopped; attempt++) {
|
|
30919
31543
|
try {
|
|
30920
31544
|
await this.connectOnce();
|
|
30921
|
-
|
|
30922
|
-
|
|
31545
|
+
if (this.sessionEstablished) {
|
|
31546
|
+
attempt = 0;
|
|
31547
|
+
hashcodeInvalidStreak = 0;
|
|
31548
|
+
}
|
|
30923
31549
|
} catch (err) {
|
|
30924
31550
|
if (err instanceof CliApiError) {
|
|
30925
31551
|
if (["HASHCODE_EXPIRED", "LINK_REVOKED"].includes(err.errorCode)) {
|
|
@@ -30935,6 +31561,8 @@ var WsClient = class {
|
|
|
30935
31561
|
`Token request rejected (HASHCODE_INVALID) \u2014 retrying (${hashcodeInvalidStreak}/${MAX_HASHCODE_INVALID_RETRIES}).`,
|
|
30936
31562
|
"warn"
|
|
30937
31563
|
);
|
|
31564
|
+
} else if (err.errorCode === "USER_PAUSED") {
|
|
31565
|
+
this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
|
|
30938
31566
|
} else {
|
|
30939
31567
|
this.bus.log(`Connection error: ${err}`, "error");
|
|
30940
31568
|
}
|
|
@@ -30958,8 +31586,9 @@ var WsClient = class {
|
|
|
30958
31586
|
* One WS session: token → connect → handshake → handle messages until close.
|
|
30959
31587
|
*/
|
|
30960
31588
|
connectOnce() {
|
|
30961
|
-
return new Promise((
|
|
31589
|
+
return new Promise((resolve6, reject) => {
|
|
30962
31590
|
void (async () => {
|
|
31591
|
+
this.sessionEstablished = false;
|
|
30963
31592
|
this.bus.setStatus("connecting");
|
|
30964
31593
|
const token = await requestWsToken(this.credential.serverUrl, this.credential.hashcode3);
|
|
30965
31594
|
this.wsToken = token.wsToken;
|
|
@@ -30973,6 +31602,17 @@ var WsClient = class {
|
|
|
30973
31602
|
timezone: token.timezone
|
|
30974
31603
|
});
|
|
30975
31604
|
}
|
|
31605
|
+
if (typeof token.cliRetentionDays === "number" && token.cliRetentionDays > 0) {
|
|
31606
|
+
logger.setRetentionDays(token.cliRetentionDays);
|
|
31607
|
+
pruneCommandHistoryByAge(token.cliRetentionDays);
|
|
31608
|
+
pruneCommandOutputByAge(token.cliRetentionDays);
|
|
31609
|
+
}
|
|
31610
|
+
writeProfileConfig(this.context.profileDir, {
|
|
31611
|
+
sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
|
|
31612
|
+
perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
|
|
31613
|
+
});
|
|
31614
|
+
this.outboundReviewEnabled = token.outboundReview?.enabled === true;
|
|
31615
|
+
this.machineUsername = token.machineUsername ?? "";
|
|
30976
31616
|
if (token.scope && token.scope !== this.credential.scope) {
|
|
30977
31617
|
this.credential.scope = token.scope;
|
|
30978
31618
|
writeCredential(this.context.profileDir, this.credential);
|
|
@@ -30980,7 +31620,12 @@ var WsClient = class {
|
|
|
30980
31620
|
} else if (token.scope) {
|
|
30981
31621
|
this.bus.setScope(token.scope);
|
|
30982
31622
|
}
|
|
30983
|
-
|
|
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";
|
|
30984
31629
|
const socket = new wrapper_default(wsUrl);
|
|
30985
31630
|
this.socket = socket;
|
|
30986
31631
|
socket.on("open", () => {
|
|
@@ -31010,7 +31655,7 @@ var WsClient = class {
|
|
|
31010
31655
|
if (code === CLOSE_DRAINING) {
|
|
31011
31656
|
this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
|
|
31012
31657
|
}
|
|
31013
|
-
|
|
31658
|
+
resolve6();
|
|
31014
31659
|
});
|
|
31015
31660
|
socket.on("error", (err) => {
|
|
31016
31661
|
this.clearHeartbeat();
|
|
@@ -31026,6 +31671,7 @@ var WsClient = class {
|
|
|
31026
31671
|
if ("type" in message) {
|
|
31027
31672
|
if (message.type === "hello_ack" && this.ecdh) {
|
|
31028
31673
|
this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
|
|
31674
|
+
this.sessionEstablished = true;
|
|
31029
31675
|
if (message.workerName) this.bus.setWorker(message.workerName);
|
|
31030
31676
|
if (message.projectName) {
|
|
31031
31677
|
this.bus.setProject(message.projectName);
|
|
@@ -31042,6 +31688,7 @@ var WsClient = class {
|
|
|
31042
31688
|
this.flushOfflineBuffer();
|
|
31043
31689
|
this.startHeartbeat();
|
|
31044
31690
|
this.startUsagePolling();
|
|
31691
|
+
this.startLogUpload();
|
|
31045
31692
|
this.connectWaiters.splice(0).forEach((fn) => fn());
|
|
31046
31693
|
return;
|
|
31047
31694
|
}
|
|
@@ -31070,6 +31717,11 @@ var WsClient = class {
|
|
|
31070
31717
|
this.socket?.close();
|
|
31071
31718
|
return;
|
|
31072
31719
|
}
|
|
31720
|
+
if (message.errorCode === "USER_PAUSED") {
|
|
31721
|
+
this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
|
|
31722
|
+
this.socket?.close();
|
|
31723
|
+
return;
|
|
31724
|
+
}
|
|
31073
31725
|
this.bus.log(`Server reported an error: ${message.errorCode}`, "warn");
|
|
31074
31726
|
this.socket?.close();
|
|
31075
31727
|
return;
|
|
@@ -31113,6 +31765,11 @@ var WsClient = class {
|
|
|
31113
31765
|
}
|
|
31114
31766
|
case WsChannels.COMMAND_DISPATCH: {
|
|
31115
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);
|
|
31116
31773
|
logger.info("command.dispatch", {
|
|
31117
31774
|
commandId: dispatch.commandId,
|
|
31118
31775
|
cmd: dispatch.cmd,
|
|
@@ -31174,32 +31831,62 @@ var WsClient = class {
|
|
|
31174
31831
|
(reply) => this.send(WsChannels.GIT_ENV, reply, message.id)
|
|
31175
31832
|
);
|
|
31176
31833
|
break;
|
|
31834
|
+
case WsChannels.GIT_REPOS:
|
|
31835
|
+
void gitRepos(this.physicRoot).then(
|
|
31836
|
+
(reply) => this.send(WsChannels.GIT_REPOS, reply, message.id)
|
|
31837
|
+
);
|
|
31838
|
+
break;
|
|
31839
|
+
case WsChannels.GIT_LOG: {
|
|
31840
|
+
const req = payload;
|
|
31841
|
+
void gitLog(this.physicRoot, req.repo ?? "", req.skip ?? 0, req.limit ?? 50).then(
|
|
31842
|
+
(reply) => this.send(WsChannels.GIT_LOG, reply, message.id)
|
|
31843
|
+
);
|
|
31844
|
+
break;
|
|
31845
|
+
}
|
|
31846
|
+
case WsChannels.GIT_COMMIT: {
|
|
31847
|
+
const req = payload;
|
|
31848
|
+
void gitCommit(this.physicRoot, req.repo ?? "", req.hash).then(
|
|
31849
|
+
(reply) => this.send(WsChannels.GIT_COMMIT, reply, message.id)
|
|
31850
|
+
);
|
|
31851
|
+
break;
|
|
31852
|
+
}
|
|
31853
|
+
case WsChannels.GIT_COMMIT_DIFF: {
|
|
31854
|
+
const req = payload;
|
|
31855
|
+
void gitCommitDiff(this.physicRoot, req.repo ?? "", req.hash, req.path).then(
|
|
31856
|
+
(reply) => this.send(WsChannels.GIT_COMMIT_DIFF, reply, message.id)
|
|
31857
|
+
);
|
|
31858
|
+
break;
|
|
31859
|
+
}
|
|
31177
31860
|
case WsChannels.LOG_READ: {
|
|
31178
31861
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
31179
31862
|
const lines = readRecentLogLines(join13(this.context.profileDir, "logs"), limit);
|
|
31180
31863
|
this.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
31181
31864
|
break;
|
|
31182
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
|
+
}
|
|
31183
31871
|
case WsChannels.PROJECT_CREATE:
|
|
31184
31872
|
void scaffoldProject(
|
|
31185
31873
|
payload,
|
|
31874
|
+
this.context.profileDir,
|
|
31186
31875
|
(p) => this.send(WsChannels.PROJECT_PROGRESS, p)
|
|
31187
31876
|
).then((reply) => this.send(WsChannels.PROJECT_CREATE, reply, message.id));
|
|
31188
31877
|
break;
|
|
31189
31878
|
case WsChannels.PROJECT_ADD:
|
|
31190
31879
|
void addProject(
|
|
31191
31880
|
payload,
|
|
31881
|
+
this.context.profileDir,
|
|
31192
31882
|
(p) => this.send(WsChannels.PROJECT_PROGRESS, p)
|
|
31193
31883
|
).then((reply) => this.send(WsChannels.PROJECT_ADD, reply, message.id));
|
|
31194
31884
|
break;
|
|
31195
|
-
|
|
31196
|
-
|
|
31197
|
-
|
|
31198
|
-
)
|
|
31199
|
-
|
|
31200
|
-
case WsChannels.AI_REVIEW:
|
|
31201
|
-
void aiReview(payload).then(
|
|
31202
|
-
(reply) => this.send(WsChannels.AI_REVIEW, reply, message.id)
|
|
31885
|
+
// AI spec-assist (project-0012/0013/0033) now runs via command.dispatch({ ai:true })
|
|
31886
|
+
// → runAiPrompt (profile-failover), not dedicated ai.* channels (ADR-0100).
|
|
31887
|
+
case WsChannels.REVIEW_EVALUATE:
|
|
31888
|
+
void evaluateReview(payload).then(
|
|
31889
|
+
(reply) => this.send(WsChannels.REVIEW_EVALUATE, reply, message.id)
|
|
31203
31890
|
);
|
|
31204
31891
|
break;
|
|
31205
31892
|
default:
|
|
@@ -31242,34 +31929,93 @@ var WsClient = class {
|
|
|
31242
31929
|
*/
|
|
31243
31930
|
async runAiPrompt(prompt, commandId, origin) {
|
|
31244
31931
|
const config = readProfileConfig(this.context.profileDir);
|
|
31932
|
+
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
31933
|
+
if (perPromptLimit > 0) {
|
|
31934
|
+
const estimated = estimateTokens(prompt);
|
|
31935
|
+
if (estimated > perPromptLimit) {
|
|
31936
|
+
this.rejectPromptOverLimit(commandId, origin, estimated, perPromptLimit);
|
|
31937
|
+
return;
|
|
31938
|
+
}
|
|
31939
|
+
}
|
|
31940
|
+
if (this.outboundReviewEnabled) {
|
|
31941
|
+
let verdict;
|
|
31942
|
+
try {
|
|
31943
|
+
verdict = await this.request(WsChannels.REVIEW_REQUEST, {
|
|
31944
|
+
commandId,
|
|
31945
|
+
prompt
|
|
31946
|
+
});
|
|
31947
|
+
} catch {
|
|
31948
|
+
verdict = { commandId, ok: false, reasons: ["unavailable"] };
|
|
31949
|
+
}
|
|
31950
|
+
if (!verdict.ok) {
|
|
31951
|
+
this.rejectByReview(commandId, origin, verdict.reasons);
|
|
31952
|
+
return;
|
|
31953
|
+
}
|
|
31954
|
+
}
|
|
31245
31955
|
const cwd2 = this.physicRoot ?? config.physicPath ?? process.cwd();
|
|
31246
31956
|
if (this.physicRoot) {
|
|
31247
31957
|
this.ensurePhysicFolderPath(this.physicRoot);
|
|
31958
|
+
await setCommitAuthor(cwd2, this.machineUsername);
|
|
31248
31959
|
} else {
|
|
31249
31960
|
this.bus.log("No project attached \u2014 running in the current directory.", "warn");
|
|
31250
31961
|
}
|
|
31251
31962
|
const cmd = config.aiCli ?? "claude";
|
|
31252
31963
|
const workingDir = getWorkingProfile(this.context.profileDir, cmd);
|
|
31253
|
-
const
|
|
31254
|
-
|
|
31964
|
+
const startDir = this.profileUnderSessionPressure(config, cmd, workingDir);
|
|
31965
|
+
if (startDir && startDir !== workingDir) {
|
|
31966
|
+
this.bus.push({
|
|
31967
|
+
source: origin,
|
|
31968
|
+
kind: "log",
|
|
31969
|
+
text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${profileDisplayLabel(startDir)}"`
|
|
31970
|
+
});
|
|
31971
|
+
}
|
|
31972
|
+
const plan = planAiRun(prompt, config, startDir ?? workingDir);
|
|
31973
|
+
const markerArgs = plan.attempts[0]?.args ?? [];
|
|
31974
|
+
this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
|
|
31255
31975
|
logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
|
|
31256
31976
|
if (origin === "local") {
|
|
31257
31977
|
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
31258
31978
|
commandId,
|
|
31259
31979
|
cmd: plan.cmd,
|
|
31260
|
-
args:
|
|
31980
|
+
args: markerArgs,
|
|
31261
31981
|
origin: "local"
|
|
31262
31982
|
});
|
|
31263
31983
|
}
|
|
31264
|
-
recordCommand({ commandId, cmd: plan.cmd, args:
|
|
31984
|
+
recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
|
|
31265
31985
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31266
31986
|
this.bus.startBusy(plan.cmd);
|
|
31267
31987
|
let seq = 0;
|
|
31988
|
+
let responseHeaderShown = false;
|
|
31989
|
+
let displayMode = "pending";
|
|
31990
|
+
let resultId = null;
|
|
31991
|
+
let resultBuf = "";
|
|
31268
31992
|
const result = await runAiFailover(plan, commandId, cwd2, {
|
|
31269
31993
|
onChunk: (text) => {
|
|
31270
|
-
|
|
31994
|
+
if (!responseHeaderShown) {
|
|
31995
|
+
this.bus.push({ source: origin, kind: "aires", text: `${plan.cmd} \u203A` });
|
|
31996
|
+
responseHeaderShown = true;
|
|
31997
|
+
}
|
|
31271
31998
|
appendCommandOutput(commandId, text);
|
|
31272
31999
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: text });
|
|
32000
|
+
if (displayMode === "result") {
|
|
32001
|
+
resultBuf += text;
|
|
32002
|
+
if (resultId) this.bus.updateEntry(resultId, resultBuf);
|
|
32003
|
+
return;
|
|
32004
|
+
}
|
|
32005
|
+
if (displayMode === "prose") {
|
|
32006
|
+
this.bus.push({ source: origin, kind: "out", text });
|
|
32007
|
+
return;
|
|
32008
|
+
}
|
|
32009
|
+
resultBuf += text;
|
|
32010
|
+
if (resultBuf.replace(/^\s+/, "").length === 0) return;
|
|
32011
|
+
const kind = looksLikeJsonOrCode(resultBuf);
|
|
32012
|
+
if (kind) {
|
|
32013
|
+
displayMode = "result";
|
|
32014
|
+
resultId = this.bus.push({ source: origin, kind: "result", text: resultBuf, resultKind: kind });
|
|
32015
|
+
} else {
|
|
32016
|
+
displayMode = "prose";
|
|
32017
|
+
this.bus.push({ source: origin, kind: "out", text: resultBuf });
|
|
32018
|
+
}
|
|
31273
32019
|
},
|
|
31274
32020
|
onAttemptStart: (label, index, total) => this.bus.push({
|
|
31275
32021
|
source: origin,
|
|
@@ -31300,7 +32046,7 @@ var WsClient = class {
|
|
|
31300
32046
|
this.send(WsChannels.COMMAND_HISTORY, {
|
|
31301
32047
|
commandId,
|
|
31302
32048
|
cmd: plan.cmd,
|
|
31303
|
-
args:
|
|
32049
|
+
args: markerArgs,
|
|
31304
32050
|
status: result.exitCode === 0 ? "done" : "failed",
|
|
31305
32051
|
exitCode: result.exitCode,
|
|
31306
32052
|
tokens: result.usage.tokens,
|
|
@@ -31324,6 +32070,79 @@ var WsClient = class {
|
|
|
31324
32070
|
level: result.exitCode === 0 ? "info" : "error"
|
|
31325
32071
|
});
|
|
31326
32072
|
}
|
|
32073
|
+
/**
|
|
32074
|
+
* Pick the Claude profile to try first when under session pressure (ADR-0081). Returns
|
|
32075
|
+
* the next candidate dir (cyclically after the current working one) when the knob is set
|
|
32076
|
+
* and the live session utilization is at/over it AND there is more than one candidate;
|
|
32077
|
+
* otherwise null (keep the normal working-first ordering). Claude-only (session % is a
|
|
32078
|
+
* Claude subscription metric).
|
|
32079
|
+
*/
|
|
32080
|
+
profileUnderSessionPressure(config, cmd, workingDir) {
|
|
32081
|
+
const threshold = config.sessionSwitchPct ?? 0;
|
|
32082
|
+
if (threshold <= 0 || cmd !== "claude") return null;
|
|
32083
|
+
const util2 = this.usageSnapshot?.session.utilizationPct ?? 0;
|
|
32084
|
+
if (util2 < threshold) return null;
|
|
32085
|
+
const dirs = claudeHomeDirs(config);
|
|
32086
|
+
if (dirs.length < 2) return null;
|
|
32087
|
+
const currentIndex = workingDir ? dirs.indexOf(workingDir) : -1;
|
|
32088
|
+
const next = dirs[(currentIndex + 1) % dirs.length];
|
|
32089
|
+
return next ?? null;
|
|
32090
|
+
}
|
|
32091
|
+
/**
|
|
32092
|
+
* Reject a prompt whose estimated tokens exceed the project's per-prompt limit (ADR-0081)
|
|
32093
|
+
* without spawning: surface it in the transcript + close the command on the server with a
|
|
32094
|
+
* PROMPT_TOKEN_LIMIT_EXCEEDED note (exit 1). No usage is metered (nothing ran).
|
|
32095
|
+
*/
|
|
32096
|
+
rejectPromptOverLimit(commandId, origin, estimated, limit) {
|
|
32097
|
+
const message = `[PROMPT_TOKEN_LIMIT_EXCEEDED] Prompt (~${estimated} tokens) exceeds the project per-prompt limit of ${limit} tokens.`;
|
|
32098
|
+
logger.warn("command.ai.prompt-limit", { commandId, origin, estimated, limit });
|
|
32099
|
+
this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
|
|
32100
|
+
if (origin === "local") {
|
|
32101
|
+
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
32102
|
+
commandId,
|
|
32103
|
+
cmd: "claude",
|
|
32104
|
+
args: [],
|
|
32105
|
+
origin: "local"
|
|
32106
|
+
});
|
|
32107
|
+
}
|
|
32108
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 0, chunk: message });
|
|
32109
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 1, chunk: "", done: true, exitCode: 1 });
|
|
32110
|
+
this.bus.push({
|
|
32111
|
+
source: origin,
|
|
32112
|
+
kind: "exit",
|
|
32113
|
+
text: "\u2717 prompt rejected \u2014 over the project per-prompt token limit",
|
|
32114
|
+
level: "error"
|
|
32115
|
+
});
|
|
32116
|
+
}
|
|
32117
|
+
/**
|
|
32118
|
+
* Reject a prompt an outbound reviewer blocked (ADR-0082) without spawning: surface the
|
|
32119
|
+
* verdict + close the command on the server. `reasons` contains only violation categories
|
|
32120
|
+
* (never secret values); a "unavailable" reason maps to no-outbound-available.
|
|
32121
|
+
*/
|
|
32122
|
+
rejectByReview(commandId, origin, reasons) {
|
|
32123
|
+
const unavailable = reasons.includes("unavailable");
|
|
32124
|
+
const code = unavailable ? "OUTBOUND_REVIEW_UNAVAILABLE" : "OUTBOUND_REVIEW_REJECTED";
|
|
32125
|
+
const detail = reasons.filter((r) => r !== "unavailable").join(", ");
|
|
32126
|
+
const message = unavailable ? `[${code}] No outbound reviewer is available \u2014 input blocked.` : `[${code}] Outbound review rejected the input${detail ? ` (${detail})` : ""}.`;
|
|
32127
|
+
logger.warn("command.ai.outbound-review", { commandId, origin, reasons });
|
|
32128
|
+
this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
|
|
32129
|
+
if (origin === "local") {
|
|
32130
|
+
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
32131
|
+
commandId,
|
|
32132
|
+
cmd: "claude",
|
|
32133
|
+
args: [],
|
|
32134
|
+
origin: "local"
|
|
32135
|
+
});
|
|
32136
|
+
}
|
|
32137
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 0, chunk: message });
|
|
32138
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 1, chunk: "", done: true, exitCode: 1 });
|
|
32139
|
+
this.bus.push({
|
|
32140
|
+
source: origin,
|
|
32141
|
+
kind: "exit",
|
|
32142
|
+
text: "\u2717 input blocked by outbound review",
|
|
32143
|
+
level: "error"
|
|
32144
|
+
});
|
|
32145
|
+
}
|
|
31327
32146
|
/**
|
|
31328
32147
|
* Send one envelope (encrypted payload + per-message wsToken — arch 0004).
|
|
31329
32148
|
* When disconnected: the running command's output is held in a buffer (512KB cap,
|
|
@@ -31350,7 +32169,7 @@ var WsClient = class {
|
|
|
31350
32169
|
* Used for quota.check before spawning an AI cli (ADR-0020).
|
|
31351
32170
|
*/
|
|
31352
32171
|
request(channel, data) {
|
|
31353
|
-
return new Promise((
|
|
32172
|
+
return new Promise((resolve6, reject) => {
|
|
31354
32173
|
if (!this.socket || !this.sessionKey) {
|
|
31355
32174
|
reject(new Error("Not connected to the server."));
|
|
31356
32175
|
return;
|
|
@@ -31370,7 +32189,7 @@ var WsClient = class {
|
|
|
31370
32189
|
}, 15e3);
|
|
31371
32190
|
timer.unref?.();
|
|
31372
32191
|
this.pending.set(envelope.id, {
|
|
31373
|
-
resolve: (value) =>
|
|
32192
|
+
resolve: (value) => resolve6(value),
|
|
31374
32193
|
timer
|
|
31375
32194
|
});
|
|
31376
32195
|
this.socket.send(JSON.stringify(envelope));
|
|
@@ -31464,6 +32283,8 @@ var WsClient = class {
|
|
|
31464
32283
|
this.heartbeat = null;
|
|
31465
32284
|
if (this.usageTimer) clearInterval(this.usageTimer);
|
|
31466
32285
|
this.usageTimer = null;
|
|
32286
|
+
if (this.logUploadTimer) clearInterval(this.logUploadTimer);
|
|
32287
|
+
this.logUploadTimer = null;
|
|
31467
32288
|
}
|
|
31468
32289
|
/**
|
|
31469
32290
|
* Poll the Claude subscription usage API and push a `machine.usage` snapshot (ADR-0072).
|
|
@@ -31476,7 +32297,7 @@ var WsClient = class {
|
|
|
31476
32297
|
const config = readProfileConfig(this.context.profileDir);
|
|
31477
32298
|
const aiCli2 = config.aiCli ?? "claude";
|
|
31478
32299
|
const working = getWorkingProfile(this.context.profileDir, aiCli2);
|
|
31479
|
-
const configured =
|
|
32300
|
+
const configured = claudeHomeDirs(config);
|
|
31480
32301
|
const dirs = working ? [working, ...configured.filter((d) => d !== working)] : configured;
|
|
31481
32302
|
const profile = working ? basename3(working) : dirs[0] ? basename3(dirs[0]) : "default";
|
|
31482
32303
|
const api = await checkClaudeUsage(dirs, aiCli2);
|
|
@@ -31501,6 +32322,29 @@ var WsClient = class {
|
|
|
31501
32322
|
this.usageTimer.unref?.();
|
|
31502
32323
|
void this.pollUsage();
|
|
31503
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
|
+
}
|
|
31504
32348
|
/**
|
|
31505
32349
|
* Logout: close WS, delete `.cre`, and prompt to pair again (Unlinked state).
|
|
31506
32350
|
*/
|
|
@@ -31584,7 +32428,7 @@ var SessionBus = class {
|
|
|
31584
32428
|
get status() {
|
|
31585
32429
|
return this.currentStatus;
|
|
31586
32430
|
}
|
|
31587
|
-
/** Producer → consumers: push one transcript entry. */
|
|
32431
|
+
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
31588
32432
|
push(input) {
|
|
31589
32433
|
const entry = {
|
|
31590
32434
|
id: randomUUID2(),
|
|
@@ -31595,6 +32439,23 @@ var SessionBus = class {
|
|
|
31595
32439
|
this.history.push(entry);
|
|
31596
32440
|
if (this.history.length > REPLAY_CAP) this.history.shift();
|
|
31597
32441
|
this.emitter.emit("transcript", entry);
|
|
32442
|
+
return entry.id;
|
|
32443
|
+
}
|
|
32444
|
+
/**
|
|
32445
|
+
* Producer → consumers: replace the text of an already-pushed entry (ADR-0108) — used to
|
|
32446
|
+
* grow a streaming `result` block in place without pushing a new line per chunk. No-op if the
|
|
32447
|
+
* entry has aged out of the replay window.
|
|
32448
|
+
*/
|
|
32449
|
+
updateEntry(id, text) {
|
|
32450
|
+
const entry = this.history.find((e) => e.id === id);
|
|
32451
|
+
if (!entry) return;
|
|
32452
|
+
entry.text = text;
|
|
32453
|
+
this.emitter.emit("transcript-update", entry);
|
|
32454
|
+
}
|
|
32455
|
+
/** Subscribe to in-place entry updates (a `result` block growing — ADR-0108). */
|
|
32456
|
+
onTranscriptUpdate(fn) {
|
|
32457
|
+
this.emitter.on("transcript-update", fn);
|
|
32458
|
+
return () => this.emitter.off("transcript-update", fn);
|
|
31598
32459
|
}
|
|
31599
32460
|
/** Shortcut for a lifecycle log line (source = system). */
|
|
31600
32461
|
log(text, level = "info") {
|
|
@@ -33699,7 +34560,7 @@ var src_default = Yoga;
|
|
|
33699
34560
|
|
|
33700
34561
|
// ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
|
|
33701
34562
|
var import_react_reconciler = __toESM(require_react_reconciler(), 1);
|
|
33702
|
-
var
|
|
34563
|
+
var import_constants10 = __toESM(require_constants(), 1);
|
|
33703
34564
|
import process3 from "process";
|
|
33704
34565
|
|
|
33705
34566
|
// ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
@@ -34941,7 +35802,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
|
|
|
34941
35802
|
scheduleTimeout: setTimeout,
|
|
34942
35803
|
cancelTimeout: clearTimeout,
|
|
34943
35804
|
noTimeout: -1,
|
|
34944
|
-
getCurrentEventPriority: () =>
|
|
35805
|
+
getCurrentEventPriority: () => import_constants10.DefaultEventPriority,
|
|
34945
35806
|
beforeActiveInstanceBlur() {
|
|
34946
35807
|
},
|
|
34947
35808
|
afterActiveInstanceBlur() {
|
|
@@ -37015,8 +37876,8 @@ var Ink = class {
|
|
|
37015
37876
|
}
|
|
37016
37877
|
}
|
|
37017
37878
|
async waitUntilExit() {
|
|
37018
|
-
this.exitPromise ||= new Promise((
|
|
37019
|
-
this.resolveExitPromise =
|
|
37879
|
+
this.exitPromise ||= new Promise((resolve6, reject) => {
|
|
37880
|
+
this.resolveExitPromise = resolve6;
|
|
37020
37881
|
this.rejectExitPromise = reject;
|
|
37021
37882
|
});
|
|
37022
37883
|
return this.exitPromise;
|
|
@@ -37513,6 +38374,16 @@ function textColor(line) {
|
|
|
37513
38374
|
if (line.kind === "exit") return "green";
|
|
37514
38375
|
return void 0;
|
|
37515
38376
|
}
|
|
38377
|
+
function AiMarkerLine({ line }) {
|
|
38378
|
+
const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
|
|
38379
|
+
const idx = line.text.indexOf(arrow);
|
|
38380
|
+
if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
|
|
38381
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
|
|
38382
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: line.text.slice(0, idx) }),
|
|
38383
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
|
|
38384
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
|
|
38385
|
+
] });
|
|
38386
|
+
}
|
|
37516
38387
|
function wrapToWidth(text, width) {
|
|
37517
38388
|
if (width <= 0 || text.length <= width) return [text];
|
|
37518
38389
|
const rows = [];
|
|
@@ -37538,20 +38409,94 @@ function wrapToWidth(text, width) {
|
|
|
37538
38409
|
if (line) rows.push(line.trimEnd());
|
|
37539
38410
|
return rows.length > 0 ? rows : [""];
|
|
37540
38411
|
}
|
|
37541
|
-
|
|
38412
|
+
var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
|
|
38413
|
+
function toSegments(raw) {
|
|
38414
|
+
if (!raw.includes("<detail")) {
|
|
38415
|
+
return raw.split("\n").map((text) => ({ text, collapsed: false }));
|
|
38416
|
+
}
|
|
38417
|
+
const out = [];
|
|
38418
|
+
const re = new RegExp(DETAIL_RE);
|
|
38419
|
+
let last = 0;
|
|
38420
|
+
let m;
|
|
38421
|
+
while ((m = re.exec(raw)) !== null) {
|
|
38422
|
+
const before = raw.slice(last, m.index).replace(/\n$/, "");
|
|
38423
|
+
if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
|
|
38424
|
+
out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
|
|
38425
|
+
last = m.index + m[0].length;
|
|
38426
|
+
if (raw[last] === "\n") last += 1;
|
|
38427
|
+
}
|
|
38428
|
+
const rest = raw.slice(last);
|
|
38429
|
+
if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
|
|
38430
|
+
if (out.length === 0) out.push({ text: "", collapsed: false });
|
|
38431
|
+
return out;
|
|
38432
|
+
}
|
|
38433
|
+
function pushCollapsible(lines, entry, opts) {
|
|
38434
|
+
const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
|
|
38435
|
+
const base = { source: entry.source, level: entry.level, kind: entry.kind };
|
|
38436
|
+
lines.push({
|
|
38437
|
+
...base,
|
|
38438
|
+
key: `${keyBase}:m`,
|
|
38439
|
+
text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
|
|
38440
|
+
showTag: firstRow,
|
|
38441
|
+
collapsed: true,
|
|
38442
|
+
spacer: false,
|
|
38443
|
+
blockIndex: blockNo
|
|
38444
|
+
});
|
|
38445
|
+
if (!expanded) return;
|
|
38446
|
+
prettyResult(body).split("\n").forEach(
|
|
38447
|
+
(seg, i) => wrapToWidth(seg, textWidth).forEach(
|
|
38448
|
+
(row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
|
|
38449
|
+
)
|
|
38450
|
+
);
|
|
38451
|
+
}
|
|
38452
|
+
function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
|
|
37542
38453
|
const lines = [];
|
|
38454
|
+
let prevKey = null;
|
|
38455
|
+
let blockNo = 0;
|
|
37543
38456
|
for (const entry of entries2) {
|
|
38457
|
+
const blockKey = `${entry.source}|${entry.kind}`;
|
|
38458
|
+
const newBlock = blockKey !== prevKey;
|
|
38459
|
+
if (newBlock && lines.length > 0) {
|
|
38460
|
+
lines.push({
|
|
38461
|
+
key: `sp:${entry.id}`,
|
|
38462
|
+
source: entry.source,
|
|
38463
|
+
level: entry.level,
|
|
38464
|
+
kind: entry.kind,
|
|
38465
|
+
text: "",
|
|
38466
|
+
showTag: false,
|
|
38467
|
+
collapsed: false,
|
|
38468
|
+
spacer: true
|
|
38469
|
+
});
|
|
38470
|
+
}
|
|
38471
|
+
prevKey = blockKey;
|
|
38472
|
+
let firstRow = newBlock;
|
|
38473
|
+
if (entry.kind === "result") {
|
|
38474
|
+
blockNo += 1;
|
|
38475
|
+
const count = prettyResult(entry.text).split("\n").length;
|
|
38476
|
+
const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
|
|
38477
|
+
pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
|
|
38478
|
+
continue;
|
|
38479
|
+
}
|
|
37544
38480
|
const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
|
|
37545
|
-
|
|
37546
|
-
|
|
37547
|
-
|
|
38481
|
+
toSegments(raw).forEach((seg, i) => {
|
|
38482
|
+
if (seg.collapsed) {
|
|
38483
|
+
blockNo += 1;
|
|
38484
|
+
pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
|
|
38485
|
+
firstRow = false;
|
|
38486
|
+
return;
|
|
38487
|
+
}
|
|
38488
|
+
wrapToWidth(seg.text, textWidth).forEach((row, j) => {
|
|
37548
38489
|
lines.push({
|
|
37549
38490
|
key: `${entry.id}:${i}:${j}`,
|
|
37550
38491
|
source: entry.source,
|
|
37551
38492
|
level: entry.level,
|
|
37552
38493
|
kind: entry.kind,
|
|
37553
|
-
text: row
|
|
38494
|
+
text: row,
|
|
38495
|
+
showTag: firstRow,
|
|
38496
|
+
collapsed: false,
|
|
38497
|
+
spacer: false
|
|
37554
38498
|
});
|
|
38499
|
+
firstRow = false;
|
|
37555
38500
|
});
|
|
37556
38501
|
});
|
|
37557
38502
|
}
|
|
@@ -37560,8 +38505,8 @@ function flattenEntries(entries2, textWidth) {
|
|
|
37560
38505
|
function TranscriptLine({ line }) {
|
|
37561
38506
|
const tag = sourceTag(line.source);
|
|
37562
38507
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
|
|
37563
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) }),
|
|
37564
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
|
|
38508
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: line.showTag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) : null }),
|
|
38509
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: line.spacer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }) : line.collapsed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, wrap: "truncate-end", children: line.text || " " }) : line.kind === "aireq" || line.kind === "aires" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AiMarkerLine, { line }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
|
|
37565
38510
|
] });
|
|
37566
38511
|
}
|
|
37567
38512
|
|
|
@@ -37573,9 +38518,9 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
37573
38518
|
import { join as join16 } from "path";
|
|
37574
38519
|
var CONFIG_KEYS = [
|
|
37575
38520
|
"aiCli",
|
|
37576
|
-
"
|
|
37577
|
-
"claudeConfigDir",
|
|
38521
|
+
"claudeHome",
|
|
37578
38522
|
"codexHome",
|
|
38523
|
+
"antigravityHome",
|
|
37579
38524
|
"physicPath",
|
|
37580
38525
|
"commandHistoryUploadMinutes",
|
|
37581
38526
|
"autoUpdate",
|
|
@@ -37591,14 +38536,13 @@ function parseConfigAssignment(key, valueTokens) {
|
|
|
37591
38536
|
case "physicPath":
|
|
37592
38537
|
if (!value) return { error: `Missing value for "${key}".` };
|
|
37593
38538
|
return { patch: { [key]: value } };
|
|
37594
|
-
case "
|
|
37595
|
-
case "codexHome":
|
|
38539
|
+
case "claudeHome":
|
|
38540
|
+
case "codexHome":
|
|
38541
|
+
case "antigravityHome": {
|
|
37596
38542
|
const dirs = valueTokens.filter((t) => t.trim());
|
|
37597
38543
|
if (dirs.length === 0) return { error: `Missing value for "${key}".` };
|
|
37598
|
-
return { patch: { [key]: dirs.
|
|
38544
|
+
return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
|
|
37599
38545
|
}
|
|
37600
|
-
case "aiArgs":
|
|
37601
|
-
return { patch: { aiArgs: valueTokens } };
|
|
37602
38546
|
case "commandHistoryUploadMinutes": {
|
|
37603
38547
|
const n = Number(value);
|
|
37604
38548
|
if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
|
|
@@ -37628,22 +38572,19 @@ function runConfig(ctx) {
|
|
|
37628
38572
|
return;
|
|
37629
38573
|
}
|
|
37630
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
|
+
};
|
|
37631
38580
|
if (existsSync10(path)) {
|
|
37632
|
-
ctx.
|
|
38581
|
+
ctx.confirm(
|
|
38582
|
+
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
38583
|
+
() => writeDefaults("replaced with")
|
|
38584
|
+
);
|
|
37633
38585
|
return;
|
|
37634
38586
|
}
|
|
37635
|
-
|
|
37636
|
-
aiCli: "claude",
|
|
37637
|
-
aiArgs: ["-p"],
|
|
37638
|
-
// Lists so failover across profiles is discoverable (ADR-0057) — add more dirs.
|
|
37639
|
-
claudeConfigDir: [".claude"],
|
|
37640
|
-
codexHome: [".codex"],
|
|
37641
|
-
autoUpdate: true,
|
|
37642
|
-
commandHistoryUploadMinutes: 10
|
|
37643
|
-
};
|
|
37644
|
-
writeProfileConfig(dir2, defaults);
|
|
37645
|
-
ctx.print(`\u2714 wrote default config: ${path}`);
|
|
37646
|
-
ctx.print("add claude profiles: /config set claudeConfigDir .claude .claude-1 (tried in order)");
|
|
38587
|
+
writeDefaults("wrote");
|
|
37647
38588
|
return;
|
|
37648
38589
|
}
|
|
37649
38590
|
if (sub === "set") {
|
|
@@ -37758,6 +38699,19 @@ function runWhoami(ctx) {
|
|
|
37758
38699
|
ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
|
|
37759
38700
|
});
|
|
37760
38701
|
}
|
|
38702
|
+
function runFold(ctx, action) {
|
|
38703
|
+
if (ctx.maxBlock === 0) {
|
|
38704
|
+
ctx.print("No collapsible blocks on screen.", "warn");
|
|
38705
|
+
return;
|
|
38706
|
+
}
|
|
38707
|
+
const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
|
|
38708
|
+
if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
|
|
38709
|
+
ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
|
|
38710
|
+
return;
|
|
38711
|
+
}
|
|
38712
|
+
if (action === "expand") ctx.expand(n);
|
|
38713
|
+
else ctx.collapse(n);
|
|
38714
|
+
}
|
|
37761
38715
|
var COMMANDS = [
|
|
37762
38716
|
{
|
|
37763
38717
|
name: "help",
|
|
@@ -37810,6 +38764,18 @@ var COMMANDS = [
|
|
|
37810
38764
|
description: "Tail the profile's structured log",
|
|
37811
38765
|
run: runLogs
|
|
37812
38766
|
},
|
|
38767
|
+
{
|
|
38768
|
+
name: "expand",
|
|
38769
|
+
usage: "/expand [N]",
|
|
38770
|
+
description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
|
|
38771
|
+
run: (ctx) => runFold(ctx, "expand")
|
|
38772
|
+
},
|
|
38773
|
+
{
|
|
38774
|
+
name: "collapse",
|
|
38775
|
+
usage: "/collapse [N]",
|
|
38776
|
+
description: "Collapse an expanded \u25B8[N] block (default: newest)",
|
|
38777
|
+
run: (ctx) => runFold(ctx, "collapse")
|
|
38778
|
+
},
|
|
37813
38779
|
{
|
|
37814
38780
|
name: "reconnect",
|
|
37815
38781
|
usage: "/reconnect",
|
|
@@ -38016,13 +38982,19 @@ function App2({
|
|
|
38016
38982
|
const [usage, setUsage] = (0, import_react26.useState)(bus.usage);
|
|
38017
38983
|
const [sessionTokens, setSessionTokens] = (0, import_react26.useState)(bus.sessionTokens);
|
|
38018
38984
|
const [scrollBack, setScrollBack] = (0, import_react26.useState)(0);
|
|
38985
|
+
const [expanded, setExpanded] = (0, import_react26.useState)(() => /* @__PURE__ */ new Set());
|
|
38019
38986
|
const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
|
|
38020
38987
|
const [startedAt] = (0, import_react26.useState)(() => Date.now());
|
|
38988
|
+
const [pendingConfirm, setPendingConfirm] = (0, import_react26.useState)(null);
|
|
38021
38989
|
(0, import_react26.useEffect)(() => {
|
|
38022
|
-
const offTranscript = bus.onTranscript(
|
|
38023
|
-
|
|
38990
|
+
const offTranscript = bus.onTranscript((entry) => {
|
|
38991
|
+
setEntries(
|
|
38024
38992
|
(prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
|
|
38025
|
-
)
|
|
38993
|
+
);
|
|
38994
|
+
if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
|
|
38995
|
+
});
|
|
38996
|
+
const offUpdate = bus.onTranscriptUpdate(
|
|
38997
|
+
(updated) => setEntries((prev) => prev.map((e) => e.id === updated.id ? { ...e, text: updated.text } : e))
|
|
38026
38998
|
);
|
|
38027
38999
|
const offStatus = bus.onStatus(setStatus);
|
|
38028
39000
|
const offBusy = bus.onBusy(setBusy);
|
|
@@ -38034,6 +39006,7 @@ function App2({
|
|
|
38034
39006
|
const offTokens = bus.onTokens(setSessionTokens);
|
|
38035
39007
|
return () => {
|
|
38036
39008
|
offTranscript();
|
|
39009
|
+
offUpdate();
|
|
38037
39010
|
offStatus();
|
|
38038
39011
|
offBusy();
|
|
38039
39012
|
offProfile();
|
|
@@ -38044,10 +39017,41 @@ function App2({
|
|
|
38044
39017
|
offTokens();
|
|
38045
39018
|
};
|
|
38046
39019
|
}, [bus]);
|
|
39020
|
+
const bodyHeight = Math.max(3, rows - CHROME_ROWS);
|
|
39021
|
+
const textWidth = Math.max(10, cols - 1 - 8);
|
|
39022
|
+
const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
|
|
39023
|
+
const maxBlock = (0, import_react26.useMemo)(() => lines.reduce((m, l) => Math.max(m, l.blockIndex ?? 0), 0), [lines]);
|
|
39024
|
+
const expandFold = (n) => {
|
|
39025
|
+
const target = n ?? maxBlock;
|
|
39026
|
+
if (target < 1 || target > maxBlock) return;
|
|
39027
|
+
setExpanded((prev) => {
|
|
39028
|
+
const next = new Set(prev);
|
|
39029
|
+
if (next.has(target)) next.delete(target);
|
|
39030
|
+
else next.add(target);
|
|
39031
|
+
return next;
|
|
39032
|
+
});
|
|
39033
|
+
};
|
|
39034
|
+
const collapseFold = (n) => {
|
|
39035
|
+
const target = n ?? maxBlock;
|
|
39036
|
+
setExpanded((prev) => {
|
|
39037
|
+
if (!prev.has(target)) return prev;
|
|
39038
|
+
const next = new Set(prev);
|
|
39039
|
+
next.delete(target);
|
|
39040
|
+
return next;
|
|
39041
|
+
});
|
|
39042
|
+
};
|
|
38047
39043
|
const handleSubmit = (input) => {
|
|
38048
39044
|
setScrollBack(0);
|
|
38049
|
-
setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
|
|
38050
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));
|
|
38051
39055
|
if (trimmed.startsWith("/")) {
|
|
38052
39056
|
bus.push({ source: "local", kind: "cmd", text: trimmed });
|
|
38053
39057
|
runSlashCommand(trimmed, {
|
|
@@ -38059,17 +39063,21 @@ function App2({
|
|
|
38059
39063
|
stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
38060
39064
|
},
|
|
38061
39065
|
quit: () => exit(),
|
|
39066
|
+
confirm: (prompt, onYes) => {
|
|
39067
|
+
bus.push({ source: "local", kind: "log", text: prompt });
|
|
39068
|
+
setPendingConfirm({ prompt, onYes });
|
|
39069
|
+
},
|
|
38062
39070
|
submitAi: (aiInput) => bus.submitLocal(aiInput),
|
|
38063
39071
|
reconnect: () => bus.requestReconnect(),
|
|
39072
|
+
expand: expandFold,
|
|
39073
|
+
collapse: collapseFold,
|
|
39074
|
+
maxBlock,
|
|
38064
39075
|
live: { status, scope, worker, project, activeProfile, startedAt }
|
|
38065
39076
|
});
|
|
38066
39077
|
return;
|
|
38067
39078
|
}
|
|
38068
39079
|
bus.submitLocal(input);
|
|
38069
39080
|
};
|
|
38070
|
-
const bodyHeight = Math.max(3, rows - CHROME_ROWS);
|
|
38071
|
-
const textWidth = Math.max(10, cols - 1 - 8);
|
|
38072
|
-
const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth), [entries2, textWidth]);
|
|
38073
39081
|
const maxScroll = Math.max(0, lines.length - bodyHeight);
|
|
38074
39082
|
const clampedBack = Math.min(scrollBack, maxScroll);
|
|
38075
39083
|
const lineRows = clampedBack > 0 ? bodyHeight - 1 : bodyHeight;
|
|
@@ -38130,6 +39138,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
38130
39138
|
base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
|
|
38131
39139
|
});
|
|
38132
39140
|
logger.info("cli.start", { serverUrl: credential.serverUrl });
|
|
39141
|
+
ensureProfileConfig(profileDir2);
|
|
38133
39142
|
const config = readProfileConfig(profileDir2);
|
|
38134
39143
|
const autoUpdate = config.autoUpdate !== false && process.env.FOURPM_NO_UPDATE !== "1";
|
|
38135
39144
|
const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
|
|
@@ -38172,6 +39181,20 @@ async function runStart(profileDir2, profileName) {
|
|
|
38172
39181
|
profileDir: profileDir2,
|
|
38173
39182
|
bus
|
|
38174
39183
|
});
|
|
39184
|
+
const runResilient = async () => {
|
|
39185
|
+
for (; ; ) {
|
|
39186
|
+
try {
|
|
39187
|
+
await client.run();
|
|
39188
|
+
return;
|
|
39189
|
+
} catch (err) {
|
|
39190
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
39191
|
+
logger.error("ws.run.crash", { message: e.message, stack: e.stack });
|
|
39192
|
+
if (client.isStopped) return;
|
|
39193
|
+
bus.log(`Recovered from an internal error \u2014 reconnecting: ${e.message}`, "error");
|
|
39194
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
39195
|
+
}
|
|
39196
|
+
}
|
|
39197
|
+
};
|
|
38175
39198
|
const useTui = Boolean(process.stdout.isTTY) && process.env.FOURPM_HEADLESS !== "1";
|
|
38176
39199
|
if (useTui) {
|
|
38177
39200
|
const info = {
|
|
@@ -38190,12 +39213,12 @@ async function runStart(profileDir2, profileName) {
|
|
|
38190
39213
|
bus.setActiveProfile(workingDir ? profileDisplayLabel(workingDir) : labels[0] ?? null);
|
|
38191
39214
|
const tui = runTui(bus, info);
|
|
38192
39215
|
bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
|
|
38193
|
-
void
|
|
39216
|
+
void runResilient();
|
|
38194
39217
|
await tui;
|
|
38195
39218
|
} else {
|
|
38196
39219
|
attachConsoleSink(bus);
|
|
38197
39220
|
bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
|
|
38198
|
-
await
|
|
39221
|
+
await runResilient();
|
|
38199
39222
|
}
|
|
38200
39223
|
} finally {
|
|
38201
39224
|
release();
|
|
@@ -38225,7 +39248,7 @@ async function runUnlink(profileDir2, profileName) {
|
|
|
38225
39248
|
|
|
38226
39249
|
// src/commands/update.ts
|
|
38227
39250
|
async function runUpdate(profileDir2, profileName, serverUrlFlag) {
|
|
38228
|
-
const serverUrl = (serverUrlFlag
|
|
39251
|
+
const serverUrl = (serverUrlFlag || readCredential(profileDir2, profileName)?.serverUrl || process.env.FOURPM_SERVER || "http://localhost:42001").replace(/\/$/, "");
|
|
38229
39252
|
let result;
|
|
38230
39253
|
try {
|
|
38231
39254
|
result = await updateToLatest(serverUrl);
|
|
@@ -38295,14 +39318,21 @@ function Picker({
|
|
|
38295
39318
|
] });
|
|
38296
39319
|
}
|
|
38297
39320
|
function selectFromList(items, title) {
|
|
38298
|
-
return new Promise((
|
|
39321
|
+
return new Promise((resolve6) => {
|
|
38299
39322
|
let chosen = null;
|
|
38300
39323
|
const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
|
|
38301
|
-
void app.waitUntilExit().then(() =>
|
|
39324
|
+
void app.waitUntilExit().then(() => resolve6(chosen));
|
|
38302
39325
|
});
|
|
38303
39326
|
}
|
|
38304
39327
|
|
|
38305
39328
|
// src/index.ts
|
|
39329
|
+
process.on("unhandledRejection", (reason) => {
|
|
39330
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
39331
|
+
logger.error("process.unhandledRejection", { message: err.message, stack: err.stack });
|
|
39332
|
+
});
|
|
39333
|
+
process.on("uncaughtException", (err) => {
|
|
39334
|
+
logger.error("process.uncaughtException", { message: err.message, stack: err.stack });
|
|
39335
|
+
});
|
|
38306
39336
|
function flagValue(args, name) {
|
|
38307
39337
|
const index = args.indexOf(`--${name}`);
|
|
38308
39338
|
return index >= 0 ? args[index + 1] : void 0;
|
|
@@ -38331,7 +39361,7 @@ async function main() {
|
|
|
38331
39361
|
const [command, ...rest] = args;
|
|
38332
39362
|
switch (command) {
|
|
38333
39363
|
case "link": {
|
|
38334
|
-
const serverUrl = flagValue(rest, "server")
|
|
39364
|
+
const serverUrl = flagValue(rest, "server") || process.env.FOURPM_SERVER || "http://localhost:42001";
|
|
38335
39365
|
await runLink(serverUrl.replace(/\/$/, ""), explicitProfile);
|
|
38336
39366
|
break;
|
|
38337
39367
|
}
|
|
@@ -38380,7 +39410,7 @@ async function main() {
|
|
|
38380
39410
|
"",
|
|
38381
39411
|
"Profiles: named after the paired account (e.g. mcacc1); when several exist,",
|
|
38382
39412
|
"start/unlink show a picker (\u2191\u2193 + Enter) \u2014 no --profile needed.",
|
|
38383
|
-
"Server URL: from FOURPM_SERVER (default http://localhost:
|
|
39413
|
+
"Server URL: from FOURPM_SERVER (default http://localhost:42001)."
|
|
38384
39414
|
].join("\n")
|
|
38385
39415
|
);
|
|
38386
39416
|
}
|