@4pm/cli 0.5.0 → 0.6.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/dist/index.js +888 -128
- 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");
|
|
@@ -24842,6 +24842,30 @@ function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
|
24842
24842
|
const exp = Math.min(baseMs * 2 ** attempt, maxMs);
|
|
24843
24843
|
return Math.floor(exp / 2 + Math.random() * (exp / 2));
|
|
24844
24844
|
}
|
|
24845
|
+
function looksLikeJsonOrCode(text) {
|
|
24846
|
+
const t = text.replace(/^\s+/, "");
|
|
24847
|
+
if (t.startsWith("```")) return "code";
|
|
24848
|
+
if (t.startsWith("{") || t.startsWith("[")) return "json";
|
|
24849
|
+
return null;
|
|
24850
|
+
}
|
|
24851
|
+
function prettyResult(text) {
|
|
24852
|
+
const trimmed = text.trim();
|
|
24853
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
24854
|
+
try {
|
|
24855
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
24856
|
+
} catch {
|
|
24857
|
+
const start = trimmed.search(/[{[]/);
|
|
24858
|
+
const end = Math.max(trimmed.lastIndexOf("}"), trimmed.lastIndexOf("]"));
|
|
24859
|
+
if (start >= 0 && end > start) {
|
|
24860
|
+
try {
|
|
24861
|
+
return JSON.stringify(JSON.parse(trimmed.slice(start, end + 1)), null, 2);
|
|
24862
|
+
} catch {
|
|
24863
|
+
}
|
|
24864
|
+
}
|
|
24865
|
+
}
|
|
24866
|
+
}
|
|
24867
|
+
return trimmed;
|
|
24868
|
+
}
|
|
24845
24869
|
|
|
24846
24870
|
// ../../22-packages/11-shared/11-constants/src/roles.ts
|
|
24847
24871
|
var Role = {
|
|
@@ -24880,6 +24904,8 @@ var PermissionCode = {
|
|
|
24880
24904
|
// ADMIN resets a sub-account's password directly (user-0010)
|
|
24881
24905
|
USER_SET_PASSWORD: "user.set_password",
|
|
24882
24906
|
USER_DELETE: "user.delete",
|
|
24907
|
+
// org-level pause/resume of a user (ADR-0093)
|
|
24908
|
+
USER_PAUSE: "user.pause",
|
|
24883
24909
|
// invite users by email (ADR-0021)
|
|
24884
24910
|
USER_INVITE: "user.invite",
|
|
24885
24911
|
// team
|
|
@@ -24904,6 +24930,11 @@ var PermissionCode = {
|
|
|
24904
24930
|
PROJECT_DETACH_USER: "project.detach_user",
|
|
24905
24931
|
PROJECT_ATTACH_MANAGER: "project.attach_manager",
|
|
24906
24932
|
PROJECT_DETACH_MANAGER: "project.detach_manager",
|
|
24933
|
+
// project lifecycle (ADR-0092): pause+resume / close+reopen
|
|
24934
|
+
PROJECT_PAUSE: "project.pause",
|
|
24935
|
+
PROJECT_CLOSE: "project.close",
|
|
24936
|
+
// project-membership pause+resume of a direct member (ADR-0093)
|
|
24937
|
+
PROJECT_PAUSE_MEMBER: "project.pause_member",
|
|
24907
24938
|
PROJECT_AI_ASSIST: "project.ai_assist",
|
|
24908
24939
|
PROJECT_MEMO_READ: "project.memo_read",
|
|
24909
24940
|
PROJECT_MEMO_WRITE: "project.memo_write",
|
|
@@ -24971,6 +25002,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
24971
25002
|
P.USER_READ,
|
|
24972
25003
|
P.USER_UPDATE,
|
|
24973
25004
|
P.USER_DELETE,
|
|
25005
|
+
P.USER_PAUSE,
|
|
24974
25006
|
P.USER_INVITE,
|
|
24975
25007
|
P.QUOTA_READ,
|
|
24976
25008
|
P.QUOTA_MANAGE,
|
|
@@ -24994,6 +25026,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
24994
25026
|
P.PROJECT_DETACH_USER,
|
|
24995
25027
|
P.PROJECT_ATTACH_MANAGER,
|
|
24996
25028
|
P.PROJECT_DETACH_MANAGER,
|
|
25029
|
+
P.PROJECT_PAUSE,
|
|
25030
|
+
P.PROJECT_CLOSE,
|
|
25031
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
24997
25032
|
P.PROJECT_AI_ASSIST,
|
|
24998
25033
|
P.PROJECT_MEMO_READ,
|
|
24999
25034
|
P.PROJECT_MEMO_WRITE,
|
|
@@ -25060,7 +25095,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
|
|
|
25060
25095
|
P.INTEGRATION_PAGE_WRITE
|
|
25061
25096
|
]
|
|
25062
25097
|
};
|
|
25063
|
-
var TL_DEFAULT_PERMISSIONS = ROLE_DEFAULT_PERMISSIONS[Role.PM].filter(
|
|
25098
|
+
var TL_DEFAULT_PERMISSIONS = ROLE_DEFAULT_PERMISSIONS[Role.PM].filter(
|
|
25099
|
+
(code) => code !== P.USER_DELETE && code !== P.USER_PAUSE
|
|
25100
|
+
);
|
|
25064
25101
|
var PROJECT_SCOPE_CHECKED_CODES = [
|
|
25065
25102
|
P.PROJECT_UPDATE,
|
|
25066
25103
|
P.PROJECT_DELETE,
|
|
@@ -25070,6 +25107,9 @@ var PROJECT_SCOPE_CHECKED_CODES = [
|
|
|
25070
25107
|
P.PROJECT_DETACH_USER,
|
|
25071
25108
|
P.PROJECT_ATTACH_MANAGER,
|
|
25072
25109
|
P.PROJECT_DETACH_MANAGER,
|
|
25110
|
+
P.PROJECT_PAUSE,
|
|
25111
|
+
P.PROJECT_CLOSE,
|
|
25112
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
25073
25113
|
P.PROJECT_AI_ASSIST
|
|
25074
25114
|
];
|
|
25075
25115
|
var SELF_SERVICE_CODES = [
|
|
@@ -25104,6 +25144,9 @@ var TEAM_GRANTABLE_CODES = [
|
|
|
25104
25144
|
P.PROJECT_DETACH_USER,
|
|
25105
25145
|
P.PROJECT_ATTACH_MANAGER,
|
|
25106
25146
|
P.PROJECT_DETACH_MANAGER,
|
|
25147
|
+
P.PROJECT_PAUSE,
|
|
25148
|
+
P.PROJECT_CLOSE,
|
|
25149
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
25107
25150
|
P.PROJECT_AI_ASSIST,
|
|
25108
25151
|
P.PROJECT_MEMO_READ,
|
|
25109
25152
|
P.PROJECT_MEMO_WRITE,
|
|
@@ -25122,6 +25165,9 @@ var PROJECT_GRANTABLE_CODES = [
|
|
|
25122
25165
|
P.PROJECT_DETACH_USER,
|
|
25123
25166
|
P.PROJECT_ATTACH_MANAGER,
|
|
25124
25167
|
P.PROJECT_DETACH_MANAGER,
|
|
25168
|
+
P.PROJECT_PAUSE,
|
|
25169
|
+
P.PROJECT_CLOSE,
|
|
25170
|
+
P.PROJECT_PAUSE_MEMBER,
|
|
25125
25171
|
P.PROJECT_AI_ASSIST,
|
|
25126
25172
|
P.PROJECT_MEMO_READ,
|
|
25127
25173
|
P.PROJECT_MEMO_WRITE,
|
|
@@ -25178,6 +25224,12 @@ var QuotaOnExceed = {
|
|
|
25178
25224
|
WARN: "warn"
|
|
25179
25225
|
};
|
|
25180
25226
|
|
|
25227
|
+
// ../../22-packages/11-shared/11-constants/src/billing.ts
|
|
25228
|
+
var PaymentProvider = {
|
|
25229
|
+
STRIPE: "stripe",
|
|
25230
|
+
PAYPAL: "paypal"
|
|
25231
|
+
};
|
|
25232
|
+
|
|
25181
25233
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
25182
25234
|
var external_exports = {};
|
|
25183
25235
|
__export(external_exports, {
|
|
@@ -29228,6 +29280,7 @@ var baseRequestSchema = external_exports.object({
|
|
|
29228
29280
|
order: external_exports.enum(["asc", "desc"]).default("desc")
|
|
29229
29281
|
});
|
|
29230
29282
|
var pinnedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
|
|
29283
|
+
var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
|
|
29231
29284
|
|
|
29232
29285
|
// ../../22-packages/11-shared/41-validation/src/schemas.ts
|
|
29233
29286
|
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 +29296,18 @@ var ipAllowlistSchema = external_exports.array(ipCidrSchema).max(50).default([])
|
|
|
29243
29296
|
var hexTokenSchema = external_exports.string().regex(/^[0-9a-f]{64}$/, "token: invalid format");
|
|
29244
29297
|
|
|
29245
29298
|
// ../../22-packages/11-shared/31-dto/src/auth.ts
|
|
29299
|
+
var paymentProviderSchema = external_exports.enum([
|
|
29300
|
+
PaymentProvider.STRIPE,
|
|
29301
|
+
PaymentProvider.PAYPAL
|
|
29302
|
+
]);
|
|
29246
29303
|
var registerRequestSchema = external_exports.object({
|
|
29247
29304
|
username: usernameSchema,
|
|
29248
29305
|
email: emailSchema,
|
|
29249
29306
|
password: passwordSchema,
|
|
29250
29307
|
phone: phoneSchema,
|
|
29251
|
-
ipAllowlist: ipAllowlistSchema.optional()
|
|
29308
|
+
ipAllowlist: ipAllowlistSchema.optional(),
|
|
29309
|
+
planCode: external_exports.string().min(1).max(50).optional(),
|
|
29310
|
+
provider: paymentProviderSchema.optional()
|
|
29252
29311
|
});
|
|
29253
29312
|
var mailTokenRequestSchema = external_exports.object({
|
|
29254
29313
|
token: hexTokenSchema
|
|
@@ -29306,7 +29365,9 @@ var rolesSchema = external_exports.array(external_exports.enum(ALL_ROLES)).min(1
|
|
|
29306
29365
|
message: "ADMIN/MACHINE cannot be combined with other roles"
|
|
29307
29366
|
});
|
|
29308
29367
|
var listUsersQuerySchema = baseRequestSchema.extend({
|
|
29309
|
-
role: external_exports.enum(ALL_ROLES).optional()
|
|
29368
|
+
role: external_exports.enum(ALL_ROLES).optional(),
|
|
29369
|
+
/** ADMIN-only Trash: list soft-deleted users (ADR-0109). */
|
|
29370
|
+
deleted: deletedFilterSchema
|
|
29310
29371
|
});
|
|
29311
29372
|
var createUserRequestSchema = external_exports.object({
|
|
29312
29373
|
username: usernameSchema,
|
|
@@ -29342,7 +29403,9 @@ var contactChangeConfirmSchema = external_exports.object({
|
|
|
29342
29403
|
|
|
29343
29404
|
// ../../22-packages/11-shared/31-dto/src/team.ts
|
|
29344
29405
|
var listTeamsQuerySchema = baseRequestSchema.extend({
|
|
29345
|
-
pinned: pinnedFilterSchema
|
|
29406
|
+
pinned: pinnedFilterSchema,
|
|
29407
|
+
/** ADMIN-only Trash: list soft-deleted teams (ADR-0109). */
|
|
29408
|
+
deleted: deletedFilterSchema
|
|
29346
29409
|
});
|
|
29347
29410
|
var createTeamRequestSchema = external_exports.object({
|
|
29348
29411
|
name: external_exports.string().min(1).max(100),
|
|
@@ -29360,7 +29423,9 @@ var attachTeamUserRequestSchema = external_exports.object({
|
|
|
29360
29423
|
|
|
29361
29424
|
// ../../22-packages/11-shared/31-dto/src/project.ts
|
|
29362
29425
|
var listProjectsQuerySchema = baseRequestSchema.extend({
|
|
29363
|
-
pinned: pinnedFilterSchema
|
|
29426
|
+
pinned: pinnedFilterSchema,
|
|
29427
|
+
/** ADMIN-only Trash: list soft-deleted projects (ADR-0109). */
|
|
29428
|
+
deleted: deletedFilterSchema
|
|
29364
29429
|
});
|
|
29365
29430
|
var recentProjectsQuerySchema = external_exports.object({
|
|
29366
29431
|
limit: external_exports.coerce.number().int().min(1).max(20).default(5)
|
|
@@ -29370,11 +29435,37 @@ var createProjectRequestSchema = external_exports.object({
|
|
|
29370
29435
|
description: external_exports.string().max(2e3).optional(),
|
|
29371
29436
|
machineUserId: external_exports.string().uuid().optional()
|
|
29372
29437
|
});
|
|
29438
|
+
var DEFAULT_OUTBOUND_RULES = {
|
|
29439
|
+
secret: [
|
|
29440
|
+
String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
|
|
29441
|
+
String.raw`/AKIA[0-9A-Z]{16}/`,
|
|
29442
|
+
String.raw`/\b(gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9]{20,})\b/`,
|
|
29443
|
+
String.raw`/-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/`,
|
|
29444
|
+
String.raw`/\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S{6,}/`
|
|
29445
|
+
],
|
|
29446
|
+
environment: [
|
|
29447
|
+
String.raw`/\b(?:ssh|scp|rsync)\s+[\w.-]+@/i`,
|
|
29448
|
+
String.raw`/\bsudo\b/i`,
|
|
29449
|
+
String.raw`/\bkubectl\b/i`,
|
|
29450
|
+
String.raw`/\bdocker\s+(?:push|-H|--host)\b/i`,
|
|
29451
|
+
String.raw`/\b(?:aws|gcloud|az)\s+\w+/i`,
|
|
29452
|
+
String.raw`/\bcurl\b[^\n]*\bhttps?:\/\/(?!localhost|127\.0\.0\.1)/i`
|
|
29453
|
+
]
|
|
29454
|
+
};
|
|
29455
|
+
function compileRulePattern(entry) {
|
|
29456
|
+
const literal = /^\/(.+)\/([gimsuy]*)$/s.exec(entry.trim());
|
|
29457
|
+
if (literal) return new RegExp(literal[1] ?? "", literal[2] ?? "");
|
|
29458
|
+
return new RegExp(entry.trim());
|
|
29459
|
+
}
|
|
29373
29460
|
var updateProjectRequestSchema = external_exports.object({
|
|
29374
29461
|
name: external_exports.string().min(1).max(100).optional(),
|
|
29375
29462
|
description: external_exports.string().max(2e3).optional(),
|
|
29376
29463
|
// Mid-tier IP allowlist (ADR-0050) — PM(manage)/ADMIN only; must fall within org.
|
|
29377
|
-
ipAllowlist: ipAllowlistSchema.optional()
|
|
29464
|
+
ipAllowlist: ipAllowlistSchema.optional(),
|
|
29465
|
+
// Messenger retention in days (0 = keep forever) — manager only (ADR-0078).
|
|
29466
|
+
messengerRetentionDays: external_exports.number().int().min(0).max(3650).optional(),
|
|
29467
|
+
// Project settings (ADR-0081) — key-level shallow-merge; `tokens` clamped on read.
|
|
29468
|
+
settings: external_exports.record(external_exports.unknown()).optional()
|
|
29378
29469
|
});
|
|
29379
29470
|
var MEMO_MAX_LENGTH = 65536;
|
|
29380
29471
|
var putMemoRequestSchema = external_exports.object({
|
|
@@ -29390,9 +29481,15 @@ var updateMemoItemSchema = external_exports.object({
|
|
|
29390
29481
|
}).refine((v) => v.content !== void 0 || v.done !== void 0, {
|
|
29391
29482
|
message: "content or done required"
|
|
29392
29483
|
});
|
|
29484
|
+
var putSpecDraftRequestSchema = external_exports.object({
|
|
29485
|
+
spec: external_exports.record(external_exports.unknown()),
|
|
29486
|
+
specVersion: external_exports.number().int().positive().optional()
|
|
29487
|
+
});
|
|
29393
29488
|
var repoSpecSchema = external_exports.object({
|
|
29394
29489
|
/** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
|
|
29395
29490
|
role: external_exports.string().max(60).optional().default(""),
|
|
29491
|
+
/** Free-form description of what this repo holds (optional). */
|
|
29492
|
+
desc: external_exports.string().max(500).optional().default(""),
|
|
29396
29493
|
/** Exactly one repo of the project is the primary. */
|
|
29397
29494
|
primary: external_exports.boolean().optional().default(false),
|
|
29398
29495
|
/** `create` a new repo (provider) or use an `existing` one (url). */
|
|
@@ -29404,14 +29501,44 @@ var repoSpecSchema = external_exports.object({
|
|
|
29404
29501
|
/** Existing repo url to clone/link (op=existing). */
|
|
29405
29502
|
url: external_exports.string().max(500).optional().default(""),
|
|
29406
29503
|
/** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
|
|
29407
|
-
subdir: external_exports.string().max(120).optional().default("")
|
|
29504
|
+
subdir: external_exports.string().max(120).optional().default(""),
|
|
29505
|
+
/** Default branch for this repo (empty ⇒ provider default, e.g. `main`). */
|
|
29506
|
+
defaultBranch: external_exports.string().max(100).optional().default(""),
|
|
29507
|
+
/** `.gitignore` template name for this repo (empty ⇒ none). */
|
|
29508
|
+
gitignore: external_exports.string().max(40).optional().default(""),
|
|
29509
|
+
/** Commit-message convention for this repo (empty ⇒ unspecified). */
|
|
29510
|
+
commitConvention: external_exports.string().max(40).optional().default("")
|
|
29511
|
+
});
|
|
29512
|
+
var specFieldEnvelopeSchema = external_exports.object({
|
|
29513
|
+
id: external_exports.string().min(1),
|
|
29514
|
+
name: external_exports.string().optional().default(""),
|
|
29515
|
+
description: external_exports.string().optional().default(""),
|
|
29516
|
+
type: external_exports.string().optional().default("text"),
|
|
29517
|
+
options: external_exports.array(external_exports.string()).optional(),
|
|
29518
|
+
value: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().default("")
|
|
29519
|
+
});
|
|
29520
|
+
var subagentSpecSchema = external_exports.object({
|
|
29521
|
+
name: external_exports.string().max(120).optional().default(""),
|
|
29522
|
+
description: external_exports.string().max(2e3).optional().default("")
|
|
29408
29523
|
});
|
|
29409
29524
|
var projectSpecSchema = external_exports.object({
|
|
29410
29525
|
specVersion: external_exports.number().int().positive(),
|
|
29411
|
-
|
|
29412
|
-
|
|
29413
|
-
|
|
29414
|
-
|
|
29526
|
+
fields: external_exports.array(specFieldEnvelopeSchema),
|
|
29527
|
+
repos: external_exports.array(repoSpecSchema).optional().default([]),
|
|
29528
|
+
subagents: external_exports.array(subagentSpecSchema).optional().default([]),
|
|
29529
|
+
meta: external_exports.record(external_exports.unknown()).optional()
|
|
29530
|
+
}).superRefine((v, ctx) => {
|
|
29531
|
+
const byId = new Map(v.fields.map((f) => [f.id, f.value]));
|
|
29532
|
+
for (const key of ["id", "name"]) {
|
|
29533
|
+
const val = byId.get(key);
|
|
29534
|
+
if (typeof val !== "string" || val.trim() === "") {
|
|
29535
|
+
ctx.addIssue({
|
|
29536
|
+
code: external_exports.ZodIssueCode.custom,
|
|
29537
|
+
path: ["fields"],
|
|
29538
|
+
message: `the "${key}" field is required`
|
|
29539
|
+
});
|
|
29540
|
+
}
|
|
29541
|
+
}
|
|
29415
29542
|
if (v.repos && v.repos.length > 0) {
|
|
29416
29543
|
const primaries = v.repos.filter((r) => r.primary).length;
|
|
29417
29544
|
if (primaries !== 1) {
|
|
@@ -29425,8 +29552,6 @@ var projectSpecSchema = external_exports.object({
|
|
|
29425
29552
|
});
|
|
29426
29553
|
var createFromSpecRequestSchema = external_exports.object({
|
|
29427
29554
|
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
29555
|
spec: projectSpecSchema
|
|
29431
29556
|
});
|
|
29432
29557
|
var addRepoRequestSchema = external_exports.object({
|
|
@@ -29436,17 +29561,14 @@ var addRepoRequestSchema = external_exports.object({
|
|
|
29436
29561
|
repoUrl: external_exports.string().url().max(500).optional(),
|
|
29437
29562
|
provider: external_exports.enum(["gh", "glab"]).optional()
|
|
29438
29563
|
});
|
|
29439
|
-
|
|
29440
|
-
|
|
29441
|
-
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
spec: external_exports.record(external_exports.unknown())
|
|
29564
|
+
|
|
29565
|
+
// ../../22-packages/11-shared/31-dto/src/activity.ts
|
|
29566
|
+
var listActivityQuerySchema = baseRequestSchema.extend({
|
|
29567
|
+
/** Filter by action prefix, e.g. "project." */
|
|
29568
|
+
action: external_exports.string().max(50).optional()
|
|
29445
29569
|
});
|
|
29446
|
-
var
|
|
29447
|
-
|
|
29448
|
-
machineLinkId: external_exports.string().uuid(),
|
|
29449
|
-
spec: external_exports.record(external_exports.unknown())
|
|
29570
|
+
var lifecycleActionRequestSchema = external_exports.object({
|
|
29571
|
+
note: external_exports.string().max(500).optional()
|
|
29450
29572
|
});
|
|
29451
29573
|
|
|
29452
29574
|
// ../../22-packages/11-shared/31-dto/src/permission.ts
|
|
@@ -29505,17 +29627,39 @@ var updateMachineLinkRequestSchema = external_exports.object({
|
|
|
29505
29627
|
});
|
|
29506
29628
|
|
|
29507
29629
|
// ../../22-packages/11-shared/31-dto/src/command.ts
|
|
29630
|
+
var COMMAND_MAX_LEN = 8e3;
|
|
29631
|
+
var AI_PROMPT_MAX_LEN = 2e5;
|
|
29508
29632
|
var dispatchCommandRequestSchema = external_exports.object({
|
|
29509
29633
|
projectId: external_exports.string().uuid(),
|
|
29510
29634
|
machineLinkId: external_exports.string().uuid(),
|
|
29511
|
-
// In AI mode `command` carries the raw prompt (can be long
|
|
29512
|
-
|
|
29635
|
+
// In AI mode `command` carries the raw prompt (can be long — the spec-assist prompts
|
|
29636
|
+
// embed the whole self-describing spec envelope, ADR-0106), else an executable name.
|
|
29637
|
+
command: external_exports.string().min(1).max(AI_PROMPT_MAX_LEN),
|
|
29513
29638
|
args: external_exports.array(external_exports.string().max(1e3)).max(50).optional(),
|
|
29514
29639
|
/**
|
|
29515
29640
|
* AI prompt mode: run `command` as a prompt through the cli's AI CLI (profile
|
|
29516
29641
|
* failover) instead of spawning it verbatim (mirrors a locally-typed prompt).
|
|
29517
29642
|
*/
|
|
29518
|
-
ai: external_exports.boolean().optional()
|
|
29643
|
+
ai: external_exports.boolean().optional(),
|
|
29644
|
+
/**
|
|
29645
|
+
* Short human label shown to OTHER tabs watching this cli's activity feed (ADR-0101):
|
|
29646
|
+
* e.g. "AI review" so the Console shows a friendly line instead of the raw prompt.
|
|
29647
|
+
*/
|
|
29648
|
+
label: external_exports.string().max(120).optional()
|
|
29649
|
+
}).superRefine((v, ctx) => {
|
|
29650
|
+
if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
|
|
29651
|
+
ctx.addIssue({
|
|
29652
|
+
code: external_exports.ZodIssueCode.too_big,
|
|
29653
|
+
maximum: COMMAND_MAX_LEN,
|
|
29654
|
+
type: "string",
|
|
29655
|
+
inclusive: true,
|
|
29656
|
+
path: ["command"],
|
|
29657
|
+
message: `command too long (max ${COMMAND_MAX_LEN})`
|
|
29658
|
+
});
|
|
29659
|
+
}
|
|
29660
|
+
});
|
|
29661
|
+
var listCommandsQuerySchema = baseRequestSchema.extend({
|
|
29662
|
+
projectId: external_exports.string().uuid()
|
|
29519
29663
|
});
|
|
29520
29664
|
|
|
29521
29665
|
// ../../22-packages/11-shared/31-dto/src/invitation.ts
|
|
@@ -29603,10 +29747,27 @@ var planLimitSchema = external_exports.object({
|
|
|
29603
29747
|
period: external_exports.enum([QuotaPeriod.DAY, QuotaPeriod.MONTH]),
|
|
29604
29748
|
onExceed: external_exports.enum([QuotaOnExceed.BLOCK, QuotaOnExceed.WARN]).default(QuotaOnExceed.BLOCK)
|
|
29605
29749
|
});
|
|
29750
|
+
var planEntitlementsSchema = external_exports.object({
|
|
29751
|
+
/** Max human seats (MACHINE excluded — ADR-0041); null = unlimited. */
|
|
29752
|
+
maxUsers: external_exports.number().int().min(1).nullable().default(null),
|
|
29753
|
+
/** Max projects; null = unlimited. */
|
|
29754
|
+
maxProjects: external_exports.number().int().min(1).nullable().default(null),
|
|
29755
|
+
/** Allowed to create teams. */
|
|
29756
|
+
teams: external_exports.boolean().default(false),
|
|
29757
|
+
/** Allowed to use Community/4rum. */
|
|
29758
|
+
community: external_exports.boolean().default(false),
|
|
29759
|
+
/** Retention cap (days) for log/command/chat; null = unlimited. */
|
|
29760
|
+
retentionDays: external_exports.number().int().min(1).nullable().default(null)
|
|
29761
|
+
});
|
|
29762
|
+
var checkoutRequestSchema = external_exports.object({
|
|
29763
|
+
planCode: external_exports.string().min(1).max(50),
|
|
29764
|
+
provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
|
|
29765
|
+
});
|
|
29606
29766
|
var upsertPlanRequestSchema = external_exports.object({
|
|
29607
29767
|
name: external_exports.string().min(1).max(100),
|
|
29608
29768
|
price: external_exports.number().int().min(0).default(0),
|
|
29609
|
-
limits: external_exports.array(planLimitSchema).default([])
|
|
29769
|
+
limits: external_exports.array(planLimitSchema).default([]),
|
|
29770
|
+
entitlements: planEntitlementsSchema.optional()
|
|
29610
29771
|
});
|
|
29611
29772
|
var assignPlanRequestSchema = external_exports.object({
|
|
29612
29773
|
planCode: external_exports.string().min(1).max(50),
|
|
@@ -29670,6 +29831,12 @@ var WsChannels = {
|
|
|
29670
29831
|
FS_READ: "fs.read",
|
|
29671
29832
|
GIT_DIFF: "git.diff",
|
|
29672
29833
|
GIT_ENV: "git.env",
|
|
29834
|
+
// git history browse (read-through cli — ADR-0089): repos on the physic project,
|
|
29835
|
+
// commit log, changed files of a commit, parent↔commit diff of one file.
|
|
29836
|
+
GIT_REPOS: "git.repos",
|
|
29837
|
+
GIT_LOG: "git.log",
|
|
29838
|
+
GIT_COMMIT: "git.commit",
|
|
29839
|
+
GIT_COMMIT_DIFF: "git.commit-diff",
|
|
29673
29840
|
PROJECT_CREATE: "project.create",
|
|
29674
29841
|
PROJECT_ADD: "project.add",
|
|
29675
29842
|
PROJECT_PROGRESS: "project.progress",
|
|
@@ -29681,8 +29848,8 @@ var WsChannels = {
|
|
|
29681
29848
|
PHYSIC_DELETE: "physic.delete",
|
|
29682
29849
|
// command lifecycle
|
|
29683
29850
|
COMMAND_ANNOUNCE: "command.announce",
|
|
29684
|
-
|
|
29685
|
-
|
|
29851
|
+
// AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
|
|
29852
|
+
// command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
|
|
29686
29853
|
// quota & usage (ADR-0020)
|
|
29687
29854
|
QUOTA_CHECK: "quota.check",
|
|
29688
29855
|
USAGE_REPORT: "usage.report",
|
|
@@ -29691,7 +29858,11 @@ var WsChannels = {
|
|
|
29691
29858
|
// cli → server: durable per-command history record on finish (ADR-0072)
|
|
29692
29859
|
COMMAND_HISTORY: "command.history",
|
|
29693
29860
|
// server → cli (reply): tail the cli's own JSONL logs on demand (ADR-0072)
|
|
29694
|
-
LOG_READ: "log.read"
|
|
29861
|
+
LOG_READ: "log.read",
|
|
29862
|
+
// outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
|
|
29863
|
+
REVIEW_REQUEST: "review.request",
|
|
29864
|
+
REVIEW_EVALUATE: "review.evaluate",
|
|
29865
|
+
REVIEW_RESULT: "review.result"
|
|
29695
29866
|
};
|
|
29696
29867
|
|
|
29697
29868
|
// ../../22-packages/21-protocol/21-websocket/src/crypto.ts
|
|
@@ -30026,7 +30197,7 @@ async function runCommand(dispatch, emit) {
|
|
|
30026
30197
|
});
|
|
30027
30198
|
child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
|
|
30028
30199
|
child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
|
|
30029
|
-
await new Promise((
|
|
30200
|
+
await new Promise((resolve6) => {
|
|
30030
30201
|
child.on("close", (exitCode) => {
|
|
30031
30202
|
batcher.flush();
|
|
30032
30203
|
emit({
|
|
@@ -30036,7 +30207,7 @@ async function runCommand(dispatch, emit) {
|
|
|
30036
30207
|
done: true,
|
|
30037
30208
|
exitCode: exitCode ?? -1
|
|
30038
30209
|
});
|
|
30039
|
-
|
|
30210
|
+
resolve6();
|
|
30040
30211
|
});
|
|
30041
30212
|
child.on("error", (err) => {
|
|
30042
30213
|
emit({
|
|
@@ -30047,7 +30218,7 @@ async function runCommand(dispatch, emit) {
|
|
|
30047
30218
|
done: true,
|
|
30048
30219
|
exitCode: -1
|
|
30049
30220
|
});
|
|
30050
|
-
|
|
30221
|
+
resolve6();
|
|
30051
30222
|
});
|
|
30052
30223
|
});
|
|
30053
30224
|
}
|
|
@@ -30127,10 +30298,10 @@ function isSessionLimit(text) {
|
|
|
30127
30298
|
);
|
|
30128
30299
|
}
|
|
30129
30300
|
function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
|
|
30130
|
-
return new Promise((
|
|
30301
|
+
return new Promise((resolve6) => {
|
|
30131
30302
|
void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
|
|
30132
30303
|
if (out.chunk) onChunk(out.chunk);
|
|
30133
|
-
if (out.done)
|
|
30304
|
+
if (out.done) resolve6(out.exitCode ?? -1);
|
|
30134
30305
|
});
|
|
30135
30306
|
});
|
|
30136
30307
|
}
|
|
@@ -30291,16 +30462,16 @@ async function fetchUsage(token) {
|
|
|
30291
30462
|
}
|
|
30292
30463
|
}
|
|
30293
30464
|
function runUsageCli(dir2, aiCli2) {
|
|
30294
|
-
return new Promise((
|
|
30465
|
+
return new Promise((resolve6) => {
|
|
30295
30466
|
let out = "";
|
|
30296
30467
|
let child = null;
|
|
30297
30468
|
const finish = () => {
|
|
30298
30469
|
clearTimeout(timer);
|
|
30299
|
-
|
|
30470
|
+
resolve6(out);
|
|
30300
30471
|
};
|
|
30301
30472
|
const timer = setTimeout(() => {
|
|
30302
30473
|
child?.kill();
|
|
30303
|
-
|
|
30474
|
+
resolve6(out);
|
|
30304
30475
|
}, 3e4);
|
|
30305
30476
|
timer.unref?.();
|
|
30306
30477
|
try {
|
|
@@ -30423,26 +30594,95 @@ async function runAi(prompt) {
|
|
|
30423
30594
|
});
|
|
30424
30595
|
return parseAi(cli, stdout, prompt);
|
|
30425
30596
|
}
|
|
30426
|
-
async function
|
|
30597
|
+
async function aiGenerate(prompt) {
|
|
30598
|
+
const { text } = await runAi(prompt);
|
|
30599
|
+
return text;
|
|
30600
|
+
}
|
|
30601
|
+
async function aiOutboundReview(input, allowedRepos, projectPath) {
|
|
30602
|
+
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:
|
|
30603
|
+
${input}`;
|
|
30604
|
+
const { text, tokens } = await runAi(prompt);
|
|
30605
|
+
const start = text.indexOf("{");
|
|
30606
|
+
const end = text.lastIndexOf("}");
|
|
30607
|
+
if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
|
|
30427
30608
|
try {
|
|
30428
|
-
const
|
|
30429
|
-
|
|
30430
|
-
|
|
30431
|
-
|
|
30432
|
-
|
|
30433
|
-
return { value: "", error: err instanceof Error ? err.message : String(err) };
|
|
30609
|
+
const j = JSON.parse(text.slice(start, end + 1));
|
|
30610
|
+
const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
|
|
30611
|
+
return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
|
|
30612
|
+
} catch {
|
|
30613
|
+
return { ok: false, reasons: ["ai_parse"], tokens };
|
|
30434
30614
|
}
|
|
30435
30615
|
}
|
|
30436
|
-
|
|
30437
|
-
|
|
30438
|
-
|
|
30439
|
-
|
|
30440
|
-
|
|
30441
|
-
|
|
30442
|
-
|
|
30443
|
-
|
|
30444
|
-
|
|
30616
|
+
|
|
30617
|
+
// src/core/outbound-review.ts
|
|
30618
|
+
function compilePatterns(sources, fallback) {
|
|
30619
|
+
const list = sources ?? fallback;
|
|
30620
|
+
const out = [];
|
|
30621
|
+
for (const src of list) {
|
|
30622
|
+
try {
|
|
30623
|
+
out.push(compileRulePattern(src));
|
|
30624
|
+
} catch (err) {
|
|
30625
|
+
logger.warn("outbound-review.bad-pattern", {
|
|
30626
|
+
pattern: src,
|
|
30627
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30628
|
+
});
|
|
30629
|
+
}
|
|
30445
30630
|
}
|
|
30631
|
+
return out;
|
|
30632
|
+
}
|
|
30633
|
+
function referencedRepos(text) {
|
|
30634
|
+
const repos = /* @__PURE__ */ new Set();
|
|
30635
|
+
const re = /(?:https?:\/\/|git@)([\w.-]+)[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?(?=[\s"')]|$)/gi;
|
|
30636
|
+
let m;
|
|
30637
|
+
while (m = re.exec(text)) repos.add(`${m[1]}/${m[2]}`.toLowerCase());
|
|
30638
|
+
return [...repos];
|
|
30639
|
+
}
|
|
30640
|
+
function ruleReview(input, allowedRepos, projectPath, secretPatterns, envPatterns) {
|
|
30641
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
30642
|
+
const secretRe = compilePatterns(secretPatterns, DEFAULT_OUTBOUND_RULES.secret);
|
|
30643
|
+
const envRe = compilePatterns(envPatterns, DEFAULT_OUTBOUND_RULES.environment);
|
|
30644
|
+
if (secretRe.some((re) => re.test(input))) reasons.add("secret");
|
|
30645
|
+
if (envRe.some((re) => re.test(input))) reasons.add("environment");
|
|
30646
|
+
if (/(?:^|[\s"'(])(?:\.\.[/\\])+/.test(input)) reasons.add("path");
|
|
30647
|
+
const absSystem = /(?:^|[\s"'(=])(\/(?:etc|root|var|usr|bin|home|private|proc|sys)\b|[A-Za-z]:\\\\)/;
|
|
30648
|
+
if (absSystem.test(input) && projectPath && !input.includes(projectPath)) reasons.add("path");
|
|
30649
|
+
if (allowedRepos.length) {
|
|
30650
|
+
const allow = allowedRepos.map((r) => r.toLowerCase());
|
|
30651
|
+
const refs = referencedRepos(input);
|
|
30652
|
+
if (refs.some((r) => !allow.some((a) => a.includes(r) || r.includes(a)))) reasons.add("repo");
|
|
30653
|
+
}
|
|
30654
|
+
return [...reasons];
|
|
30655
|
+
}
|
|
30656
|
+
async function evaluateReview(payload) {
|
|
30657
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
30658
|
+
let tokens = 0;
|
|
30659
|
+
if (payload.ruleCheck) {
|
|
30660
|
+
const violations = ruleReview(
|
|
30661
|
+
payload.prompt,
|
|
30662
|
+
payload.allowedRepos,
|
|
30663
|
+
payload.projectPath ?? "",
|
|
30664
|
+
payload.secretPatterns,
|
|
30665
|
+
payload.envPatterns
|
|
30666
|
+
);
|
|
30667
|
+
for (const r of violations) reasons.add(r);
|
|
30668
|
+
}
|
|
30669
|
+
if (payload.aiReview) {
|
|
30670
|
+
try {
|
|
30671
|
+
const ai = await aiOutboundReview(
|
|
30672
|
+
payload.prompt,
|
|
30673
|
+
payload.allowedRepos,
|
|
30674
|
+
payload.projectPath ?? ""
|
|
30675
|
+
);
|
|
30676
|
+
tokens = ai.tokens;
|
|
30677
|
+
if (!ai.ok) for (const r of ai.reasons) reasons.add(r);
|
|
30678
|
+
} catch (err) {
|
|
30679
|
+
logger.warn("outbound-review.ai-error", {
|
|
30680
|
+
commandId: payload.commandId,
|
|
30681
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30682
|
+
});
|
|
30683
|
+
}
|
|
30684
|
+
}
|
|
30685
|
+
return { commandId: payload.commandId, ok: reasons.size === 0, reasons: [...reasons], tokens };
|
|
30446
30686
|
}
|
|
30447
30687
|
|
|
30448
30688
|
// src/core/command-history.ts
|
|
@@ -30703,13 +30943,142 @@ ${stderr}`;
|
|
|
30703
30943
|
return { installed, authenticated, account, claudeCli };
|
|
30704
30944
|
}
|
|
30705
30945
|
|
|
30706
|
-
// src/core/
|
|
30707
|
-
import {
|
|
30946
|
+
// src/core/git-history.ts
|
|
30947
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
30708
30948
|
import { execFile as execFile4 } from "child_process";
|
|
30709
|
-
import {
|
|
30710
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
30949
|
+
import { resolve as resolve4, sep as sep3 } from "path";
|
|
30711
30950
|
import { promisify as promisify4 } from "util";
|
|
30712
30951
|
var run4 = promisify4(execFile4);
|
|
30952
|
+
var FS = "";
|
|
30953
|
+
var RS = "";
|
|
30954
|
+
function resolveRepo(root, repo) {
|
|
30955
|
+
if (!root) return null;
|
|
30956
|
+
const base = resolve4(root);
|
|
30957
|
+
const requested = resolve4(base, repo && repo.trim() ? repo : ".");
|
|
30958
|
+
return requested === base || requested.startsWith(base + sep3) ? requested : base;
|
|
30959
|
+
}
|
|
30960
|
+
async function git(cwd2, args) {
|
|
30961
|
+
try {
|
|
30962
|
+
const { stdout } = await run4("git", args, {
|
|
30963
|
+
cwd: cwd2,
|
|
30964
|
+
timeout: 15e3,
|
|
30965
|
+
maxBuffer: 8 * 1024 * 1024
|
|
30966
|
+
});
|
|
30967
|
+
return stdout;
|
|
30968
|
+
} catch {
|
|
30969
|
+
return "";
|
|
30970
|
+
}
|
|
30971
|
+
}
|
|
30972
|
+
async function gitRepos(root) {
|
|
30973
|
+
if (!root) return { repos: [] };
|
|
30974
|
+
const base = resolve4(root);
|
|
30975
|
+
const found = /* @__PURE__ */ new Set();
|
|
30976
|
+
const repos = [];
|
|
30977
|
+
const add = async (dir2) => {
|
|
30978
|
+
const inside = await git(dir2, ["rev-parse", "--show-toplevel"]);
|
|
30979
|
+
const top = inside.trim();
|
|
30980
|
+
if (!top || found.has(top) || !(top === base || top.startsWith(base + sep3))) return;
|
|
30981
|
+
found.add(top);
|
|
30982
|
+
const subdir = top === base ? "" : top.slice(base.length + 1);
|
|
30983
|
+
const remote = (await git(top, ["remote", "get-url", "origin"])).trim();
|
|
30984
|
+
repos.push({ subdir, name: subdir || "", remote: remote || null });
|
|
30985
|
+
};
|
|
30986
|
+
await add(base);
|
|
30987
|
+
try {
|
|
30988
|
+
const lvl1 = await readdir3(base, { withFileTypes: true });
|
|
30989
|
+
for (const d of lvl1) {
|
|
30990
|
+
if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
|
|
30991
|
+
const p1 = resolve4(base, d.name);
|
|
30992
|
+
await add(p1);
|
|
30993
|
+
try {
|
|
30994
|
+
const lvl2 = await readdir3(p1, { withFileTypes: true });
|
|
30995
|
+
for (const d2 of lvl2) {
|
|
30996
|
+
if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
|
|
30997
|
+
await add(resolve4(p1, d2.name));
|
|
30998
|
+
}
|
|
30999
|
+
} catch {
|
|
31000
|
+
}
|
|
31001
|
+
}
|
|
31002
|
+
} catch {
|
|
31003
|
+
}
|
|
31004
|
+
repos.sort((a, b) => a.subdir.localeCompare(b.subdir));
|
|
31005
|
+
return { repos };
|
|
31006
|
+
}
|
|
31007
|
+
async function gitLog(root, repo, skip, limit) {
|
|
31008
|
+
const cwd2 = resolveRepo(root, repo);
|
|
31009
|
+
if (!cwd2) return { entries: [], hasMore: false };
|
|
31010
|
+
const capped = Math.min(Math.max(limit, 1), 200);
|
|
31011
|
+
const format = ["%H", "%h", "%an", "%aI", "%s"].join(FS) + RS;
|
|
31012
|
+
const stdout = await git(cwd2, [
|
|
31013
|
+
"log",
|
|
31014
|
+
`--skip=${Math.max(skip, 0)}`,
|
|
31015
|
+
`-n`,
|
|
31016
|
+
String(capped + 1),
|
|
31017
|
+
`--pretty=format:${format}`
|
|
31018
|
+
]);
|
|
31019
|
+
const rows = stdout.split(RS).map((r) => r.replace(/^\n/, "")).filter((r) => r.length > 0);
|
|
31020
|
+
const entries2 = rows.slice(0, capped).map((row) => {
|
|
31021
|
+
const [hash = "", shortHash = "", author = "", date = "", subject = ""] = row.split(FS);
|
|
31022
|
+
return { hash, shortHash, author, date, subject };
|
|
31023
|
+
});
|
|
31024
|
+
return { entries: entries2, hasMore: rows.length > capped };
|
|
31025
|
+
}
|
|
31026
|
+
async function gitCommit(root, repo, hash) {
|
|
31027
|
+
const cwd2 = resolveRepo(root, repo);
|
|
31028
|
+
const empty = { hash, author: "", date: "", subject: "", files: [] };
|
|
31029
|
+
if (!cwd2 || !hash) return empty;
|
|
31030
|
+
const format = ["%H", "%an", "%aI", "%s"].join(FS);
|
|
31031
|
+
const stdout = await git(cwd2, [
|
|
31032
|
+
"show",
|
|
31033
|
+
"--name-status",
|
|
31034
|
+
"-M",
|
|
31035
|
+
`--pretty=format:${format}`,
|
|
31036
|
+
hash
|
|
31037
|
+
]);
|
|
31038
|
+
if (!stdout) return empty;
|
|
31039
|
+
const lines = stdout.split("\n");
|
|
31040
|
+
const [full = hash, author = "", date = "", subject = ""] = (lines[0] ?? "").split(FS);
|
|
31041
|
+
const files = [];
|
|
31042
|
+
for (const line of lines.slice(1)) {
|
|
31043
|
+
if (!line.trim()) continue;
|
|
31044
|
+
const parts = line.split(" ");
|
|
31045
|
+
const status = (parts[0] ?? "").charAt(0);
|
|
31046
|
+
if (!status) continue;
|
|
31047
|
+
if (status === "R" || status === "C") {
|
|
31048
|
+
files.push({ path: parts[2] ?? "", status, oldPath: parts[1] ?? void 0 });
|
|
31049
|
+
} else {
|
|
31050
|
+
files.push({ path: parts[1] ?? "", status });
|
|
31051
|
+
}
|
|
31052
|
+
}
|
|
31053
|
+
return { hash: full, author, date, subject, files };
|
|
31054
|
+
}
|
|
31055
|
+
async function gitCommitDiff(root, repo, hash, path) {
|
|
31056
|
+
const cwd2 = resolveRepo(root, repo);
|
|
31057
|
+
if (!cwd2 || !hash || !path) return { path, oldContent: "", newContent: "" };
|
|
31058
|
+
const oldContent = await git(cwd2, ["show", `${hash}^:${path}`]);
|
|
31059
|
+
const newContent = await git(cwd2, ["show", `${hash}:${path}`]);
|
|
31060
|
+
return { path, oldContent, newContent };
|
|
31061
|
+
}
|
|
31062
|
+
|
|
31063
|
+
// src/core/git-commit-identity.ts
|
|
31064
|
+
import { execFile as execFile5 } from "child_process";
|
|
31065
|
+
import { promisify as promisify5 } from "util";
|
|
31066
|
+
var run5 = promisify5(execFile5);
|
|
31067
|
+
async function setCommitAuthor(cwd2, machineUsername) {
|
|
31068
|
+
if (!machineUsername) return;
|
|
31069
|
+
try {
|
|
31070
|
+
await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
31071
|
+
} catch {
|
|
31072
|
+
}
|
|
31073
|
+
}
|
|
31074
|
+
|
|
31075
|
+
// src/core/scaffold.ts
|
|
31076
|
+
import { cp, mkdir as mkdir2, writeFile } from "fs/promises";
|
|
31077
|
+
import { execFile as execFile6 } from "child_process";
|
|
31078
|
+
import { dirname as dirname4, join as join12, resolve as resolve5 } from "path";
|
|
31079
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
31080
|
+
import { promisify as promisify6 } from "util";
|
|
31081
|
+
var run6 = promisify6(execFile6);
|
|
30713
31082
|
function reposOf(spec) {
|
|
30714
31083
|
const repos = spec?.repos;
|
|
30715
31084
|
return Array.isArray(repos) ? repos : [];
|
|
@@ -30722,9 +31091,9 @@ async function provisionRepos(target, repos, emit) {
|
|
|
30722
31091
|
for (const repo of repos) {
|
|
30723
31092
|
if (repo.primary) {
|
|
30724
31093
|
emit("git", "Initializing the primary repo\u2026");
|
|
30725
|
-
await
|
|
31094
|
+
await run6("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
30726
31095
|
if (repo.op === "existing" && repo.url) {
|
|
30727
|
-
await
|
|
31096
|
+
await run6("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
30728
31097
|
() => void 0
|
|
30729
31098
|
);
|
|
30730
31099
|
}
|
|
@@ -30733,23 +31102,24 @@ async function provisionRepos(target, repos, emit) {
|
|
|
30733
31102
|
const dir2 = join12(target, subDirName(repo));
|
|
30734
31103
|
if (repo.op === "existing" && repo.url) {
|
|
30735
31104
|
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
30736
|
-
await
|
|
31105
|
+
await run6("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
30737
31106
|
} else {
|
|
30738
31107
|
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
30739
31108
|
await mkdir2(dir2, { recursive: true });
|
|
30740
|
-
await
|
|
31109
|
+
await run6("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
30741
31110
|
}
|
|
30742
31111
|
}
|
|
30743
31112
|
}
|
|
30744
31113
|
function sampleDir() {
|
|
30745
31114
|
if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
|
|
30746
31115
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
30747
|
-
return
|
|
31116
|
+
return resolve5(here, "../../project-sample");
|
|
30748
31117
|
}
|
|
30749
|
-
async function scaffoldProject(payload, onProgress) {
|
|
31118
|
+
async function scaffoldProject(payload, profileDir2, onProgress) {
|
|
30750
31119
|
const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
|
|
30751
31120
|
try {
|
|
30752
|
-
const
|
|
31121
|
+
const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
|
|
31122
|
+
const target = join12(root, payload.projectName);
|
|
30753
31123
|
await mkdir2(target, { recursive: true });
|
|
30754
31124
|
emit("copy", "Copying the sample template\u2026");
|
|
30755
31125
|
await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
|
|
@@ -30763,23 +31133,60 @@ async function scaffoldProject(payload, onProgress) {
|
|
|
30763
31133
|
}
|
|
30764
31134
|
const repos = reposOf(payload.spec);
|
|
30765
31135
|
if (repos.length > 0) await provisionRepos(target, repos, emit);
|
|
31136
|
+
if (payload.spec) await aiInit(target, payload.spec, emit);
|
|
30766
31137
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
|
|
30767
31138
|
return { ok: true, path: target };
|
|
30768
31139
|
} catch (err) {
|
|
30769
31140
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
30770
31141
|
}
|
|
30771
31142
|
}
|
|
31143
|
+
async function aiInit(target, spec, emit) {
|
|
31144
|
+
emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
|
|
31145
|
+
const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
|
|
31146
|
+
if (subagents.length > 0) {
|
|
31147
|
+
await mkdir2(join12(target, ".claude", "agents"), { recursive: true });
|
|
31148
|
+
for (const sa of subagents) {
|
|
31149
|
+
const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31150
|
+
if (!name) continue;
|
|
31151
|
+
const body = `---
|
|
31152
|
+
name: ${name}
|
|
31153
|
+
---
|
|
31154
|
+
|
|
31155
|
+
${(sa.description || "").trim()}
|
|
31156
|
+
`;
|
|
31157
|
+
await writeFile(join12(target, ".claude", "agents", `${name}.md`), body, "utf8");
|
|
31158
|
+
}
|
|
31159
|
+
}
|
|
31160
|
+
const specJson = JSON.stringify(spec);
|
|
31161
|
+
await generateFile(
|
|
31162
|
+
join12(target, "README.md"),
|
|
31163
|
+
`Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
|
|
31164
|
+
${specJson}`
|
|
31165
|
+
);
|
|
31166
|
+
await generateFile(
|
|
31167
|
+
join12(target, "CLAUDE.md"),
|
|
31168
|
+
`Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
|
|
31169
|
+
${specJson}`
|
|
31170
|
+
);
|
|
31171
|
+
}
|
|
31172
|
+
async function generateFile(path, prompt) {
|
|
31173
|
+
try {
|
|
31174
|
+
const text = await aiGenerate(prompt);
|
|
31175
|
+
if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
|
|
31176
|
+
} catch {
|
|
31177
|
+
}
|
|
31178
|
+
}
|
|
30772
31179
|
async function addProject(payload, onProgress) {
|
|
30773
31180
|
const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
|
|
30774
31181
|
try {
|
|
30775
|
-
const target =
|
|
31182
|
+
const target = resolve5(payload.path);
|
|
30776
31183
|
await mkdir2(target, { recursive: true });
|
|
30777
31184
|
if (payload.repoUrl) {
|
|
30778
31185
|
emit("clone", `Cloning ${payload.repoUrl}\u2026`);
|
|
30779
|
-
await
|
|
31186
|
+
await run6("git", ["clone", payload.repoUrl, target], { timeout: 12e4 });
|
|
30780
31187
|
} else {
|
|
30781
31188
|
emit("init", "git init\u2026");
|
|
30782
|
-
await
|
|
31189
|
+
await run6("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
30783
31190
|
}
|
|
30784
31191
|
onProgress?.({ projectId: payload.projectId, step: "done", message: "Repo ready.", done: true });
|
|
30785
31192
|
return { ok: true, path: target };
|
|
@@ -30823,6 +31230,8 @@ var WsClient = class {
|
|
|
30823
31230
|
heartbeat = null;
|
|
30824
31231
|
usageTimer = null;
|
|
30825
31232
|
usageSnapshot = null;
|
|
31233
|
+
/** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
|
|
31234
|
+
outboundReviewEnabled = false;
|
|
30826
31235
|
stopped = false;
|
|
30827
31236
|
/** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
|
|
30828
31237
|
wakeReconnect = null;
|
|
@@ -30836,6 +31245,9 @@ var WsClient = class {
|
|
|
30836
31245
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
30837
31246
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
30838
31247
|
physicRoot = null;
|
|
31248
|
+
/** This machine-user's username — the git commit author for this project (ADR-0097).
|
|
31249
|
+
* Push uses the account already logged in with gh/glab on the worker. */
|
|
31250
|
+
machineUsername = "";
|
|
30839
31251
|
/** Waiters resolved when the session next becomes connected (readiness gate before
|
|
30840
31252
|
* running a local prompt). */
|
|
30841
31253
|
connectWaiters = [];
|
|
@@ -30850,6 +31262,11 @@ var WsClient = class {
|
|
|
30850
31262
|
get bus() {
|
|
30851
31263
|
return this.context.bus;
|
|
30852
31264
|
}
|
|
31265
|
+
/** True once the session stopped for good (logout / replaced / version) — the
|
|
31266
|
+
* resilient run wrapper uses this to tell a clean stop from a crash to recover from. */
|
|
31267
|
+
get isStopped() {
|
|
31268
|
+
return this.stopped;
|
|
31269
|
+
}
|
|
30853
31270
|
/**
|
|
30854
31271
|
* Force an immediate reconnect: wake the backoff sleep if waiting, else drop the live
|
|
30855
31272
|
* socket so the run() loop reconnects (the next backoff is skipped). No-op if stopped.
|
|
@@ -30870,14 +31287,14 @@ var WsClient = class {
|
|
|
30870
31287
|
*/
|
|
30871
31288
|
awaitConnected(timeoutMs) {
|
|
30872
31289
|
if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
|
|
30873
|
-
return new Promise((
|
|
31290
|
+
return new Promise((resolve6) => {
|
|
30874
31291
|
const waiter = () => {
|
|
30875
31292
|
clearTimeout(timer);
|
|
30876
|
-
|
|
31293
|
+
resolve6(true);
|
|
30877
31294
|
};
|
|
30878
31295
|
const timer = setTimeout(() => {
|
|
30879
31296
|
this.connectWaiters = this.connectWaiters.filter((w) => w !== waiter);
|
|
30880
|
-
|
|
31297
|
+
resolve6(false);
|
|
30881
31298
|
}, timeoutMs);
|
|
30882
31299
|
this.connectWaiters.push(waiter);
|
|
30883
31300
|
});
|
|
@@ -30896,17 +31313,16 @@ var WsClient = class {
|
|
|
30896
31313
|
this.forceReconnect = false;
|
|
30897
31314
|
return Promise.resolve();
|
|
30898
31315
|
}
|
|
30899
|
-
return new Promise((
|
|
31316
|
+
return new Promise((resolve6) => {
|
|
30900
31317
|
const timer = setTimeout(() => {
|
|
30901
31318
|
this.wakeReconnect = null;
|
|
30902
|
-
|
|
31319
|
+
resolve6();
|
|
30903
31320
|
}, ms);
|
|
30904
|
-
timer.unref?.();
|
|
30905
31321
|
this.wakeReconnect = () => {
|
|
30906
31322
|
clearTimeout(timer);
|
|
30907
31323
|
this.wakeReconnect = null;
|
|
30908
31324
|
this.forceReconnect = false;
|
|
30909
|
-
|
|
31325
|
+
resolve6();
|
|
30910
31326
|
};
|
|
30911
31327
|
});
|
|
30912
31328
|
}
|
|
@@ -30935,6 +31351,8 @@ var WsClient = class {
|
|
|
30935
31351
|
`Token request rejected (HASHCODE_INVALID) \u2014 retrying (${hashcodeInvalidStreak}/${MAX_HASHCODE_INVALID_RETRIES}).`,
|
|
30936
31352
|
"warn"
|
|
30937
31353
|
);
|
|
31354
|
+
} else if (err.errorCode === "USER_PAUSED") {
|
|
31355
|
+
this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
|
|
30938
31356
|
} else {
|
|
30939
31357
|
this.bus.log(`Connection error: ${err}`, "error");
|
|
30940
31358
|
}
|
|
@@ -30958,7 +31376,7 @@ var WsClient = class {
|
|
|
30958
31376
|
* One WS session: token → connect → handshake → handle messages until close.
|
|
30959
31377
|
*/
|
|
30960
31378
|
connectOnce() {
|
|
30961
|
-
return new Promise((
|
|
31379
|
+
return new Promise((resolve6, reject) => {
|
|
30962
31380
|
void (async () => {
|
|
30963
31381
|
this.bus.setStatus("connecting");
|
|
30964
31382
|
const token = await requestWsToken(this.credential.serverUrl, this.credential.hashcode3);
|
|
@@ -30973,6 +31391,12 @@ var WsClient = class {
|
|
|
30973
31391
|
timezone: token.timezone
|
|
30974
31392
|
});
|
|
30975
31393
|
}
|
|
31394
|
+
writeProfileConfig(this.context.profileDir, {
|
|
31395
|
+
sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
|
|
31396
|
+
perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
|
|
31397
|
+
});
|
|
31398
|
+
this.outboundReviewEnabled = token.outboundReview?.enabled === true;
|
|
31399
|
+
this.machineUsername = token.machineUsername ?? "";
|
|
30976
31400
|
if (token.scope && token.scope !== this.credential.scope) {
|
|
30977
31401
|
this.credential.scope = token.scope;
|
|
30978
31402
|
writeCredential(this.context.profileDir, this.credential);
|
|
@@ -31010,7 +31434,7 @@ var WsClient = class {
|
|
|
31010
31434
|
if (code === CLOSE_DRAINING) {
|
|
31011
31435
|
this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
|
|
31012
31436
|
}
|
|
31013
|
-
|
|
31437
|
+
resolve6();
|
|
31014
31438
|
});
|
|
31015
31439
|
socket.on("error", (err) => {
|
|
31016
31440
|
this.clearHeartbeat();
|
|
@@ -31070,6 +31494,11 @@ var WsClient = class {
|
|
|
31070
31494
|
this.socket?.close();
|
|
31071
31495
|
return;
|
|
31072
31496
|
}
|
|
31497
|
+
if (message.errorCode === "USER_PAUSED") {
|
|
31498
|
+
this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
|
|
31499
|
+
this.socket?.close();
|
|
31500
|
+
return;
|
|
31501
|
+
}
|
|
31073
31502
|
this.bus.log(`Server reported an error: ${message.errorCode}`, "warn");
|
|
31074
31503
|
this.socket?.close();
|
|
31075
31504
|
return;
|
|
@@ -31174,6 +31603,32 @@ var WsClient = class {
|
|
|
31174
31603
|
(reply) => this.send(WsChannels.GIT_ENV, reply, message.id)
|
|
31175
31604
|
);
|
|
31176
31605
|
break;
|
|
31606
|
+
case WsChannels.GIT_REPOS:
|
|
31607
|
+
void gitRepos(this.physicRoot).then(
|
|
31608
|
+
(reply) => this.send(WsChannels.GIT_REPOS, reply, message.id)
|
|
31609
|
+
);
|
|
31610
|
+
break;
|
|
31611
|
+
case WsChannels.GIT_LOG: {
|
|
31612
|
+
const req = payload;
|
|
31613
|
+
void gitLog(this.physicRoot, req.repo ?? "", req.skip ?? 0, req.limit ?? 50).then(
|
|
31614
|
+
(reply) => this.send(WsChannels.GIT_LOG, reply, message.id)
|
|
31615
|
+
);
|
|
31616
|
+
break;
|
|
31617
|
+
}
|
|
31618
|
+
case WsChannels.GIT_COMMIT: {
|
|
31619
|
+
const req = payload;
|
|
31620
|
+
void gitCommit(this.physicRoot, req.repo ?? "", req.hash).then(
|
|
31621
|
+
(reply) => this.send(WsChannels.GIT_COMMIT, reply, message.id)
|
|
31622
|
+
);
|
|
31623
|
+
break;
|
|
31624
|
+
}
|
|
31625
|
+
case WsChannels.GIT_COMMIT_DIFF: {
|
|
31626
|
+
const req = payload;
|
|
31627
|
+
void gitCommitDiff(this.physicRoot, req.repo ?? "", req.hash, req.path).then(
|
|
31628
|
+
(reply) => this.send(WsChannels.GIT_COMMIT_DIFF, reply, message.id)
|
|
31629
|
+
);
|
|
31630
|
+
break;
|
|
31631
|
+
}
|
|
31177
31632
|
case WsChannels.LOG_READ: {
|
|
31178
31633
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
31179
31634
|
const lines = readRecentLogLines(join13(this.context.profileDir, "logs"), limit);
|
|
@@ -31183,6 +31638,7 @@ var WsClient = class {
|
|
|
31183
31638
|
case WsChannels.PROJECT_CREATE:
|
|
31184
31639
|
void scaffoldProject(
|
|
31185
31640
|
payload,
|
|
31641
|
+
this.context.profileDir,
|
|
31186
31642
|
(p) => this.send(WsChannels.PROJECT_PROGRESS, p)
|
|
31187
31643
|
).then((reply) => this.send(WsChannels.PROJECT_CREATE, reply, message.id));
|
|
31188
31644
|
break;
|
|
@@ -31192,14 +31648,11 @@ var WsClient = class {
|
|
|
31192
31648
|
(p) => this.send(WsChannels.PROJECT_PROGRESS, p)
|
|
31193
31649
|
).then((reply) => this.send(WsChannels.PROJECT_ADD, reply, message.id));
|
|
31194
31650
|
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)
|
|
31651
|
+
// AI spec-assist (project-0012/0013/0033) now runs via command.dispatch({ ai:true })
|
|
31652
|
+
// → runAiPrompt (profile-failover), not dedicated ai.* channels (ADR-0100).
|
|
31653
|
+
case WsChannels.REVIEW_EVALUATE:
|
|
31654
|
+
void evaluateReview(payload).then(
|
|
31655
|
+
(reply) => this.send(WsChannels.REVIEW_EVALUATE, reply, message.id)
|
|
31203
31656
|
);
|
|
31204
31657
|
break;
|
|
31205
31658
|
default:
|
|
@@ -31242,16 +31695,48 @@ var WsClient = class {
|
|
|
31242
31695
|
*/
|
|
31243
31696
|
async runAiPrompt(prompt, commandId, origin) {
|
|
31244
31697
|
const config = readProfileConfig(this.context.profileDir);
|
|
31698
|
+
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
31699
|
+
if (perPromptLimit > 0) {
|
|
31700
|
+
const estimated = estimateTokens(prompt);
|
|
31701
|
+
if (estimated > perPromptLimit) {
|
|
31702
|
+
this.rejectPromptOverLimit(commandId, origin, estimated, perPromptLimit);
|
|
31703
|
+
return;
|
|
31704
|
+
}
|
|
31705
|
+
}
|
|
31706
|
+
if (this.outboundReviewEnabled) {
|
|
31707
|
+
let verdict;
|
|
31708
|
+
try {
|
|
31709
|
+
verdict = await this.request(WsChannels.REVIEW_REQUEST, {
|
|
31710
|
+
commandId,
|
|
31711
|
+
prompt
|
|
31712
|
+
});
|
|
31713
|
+
} catch {
|
|
31714
|
+
verdict = { commandId, ok: false, reasons: ["unavailable"] };
|
|
31715
|
+
}
|
|
31716
|
+
if (!verdict.ok) {
|
|
31717
|
+
this.rejectByReview(commandId, origin, verdict.reasons);
|
|
31718
|
+
return;
|
|
31719
|
+
}
|
|
31720
|
+
}
|
|
31245
31721
|
const cwd2 = this.physicRoot ?? config.physicPath ?? process.cwd();
|
|
31246
31722
|
if (this.physicRoot) {
|
|
31247
31723
|
this.ensurePhysicFolderPath(this.physicRoot);
|
|
31724
|
+
await setCommitAuthor(cwd2, this.machineUsername);
|
|
31248
31725
|
} else {
|
|
31249
31726
|
this.bus.log("No project attached \u2014 running in the current directory.", "warn");
|
|
31250
31727
|
}
|
|
31251
31728
|
const cmd = config.aiCli ?? "claude";
|
|
31252
31729
|
const workingDir = getWorkingProfile(this.context.profileDir, cmd);
|
|
31253
|
-
const
|
|
31254
|
-
|
|
31730
|
+
const startDir = this.profileUnderSessionPressure(config, cmd, workingDir);
|
|
31731
|
+
if (startDir && startDir !== workingDir) {
|
|
31732
|
+
this.bus.push({
|
|
31733
|
+
source: origin,
|
|
31734
|
+
kind: "log",
|
|
31735
|
+
text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${profileDisplayLabel(startDir)}"`
|
|
31736
|
+
});
|
|
31737
|
+
}
|
|
31738
|
+
const plan = planAiRun(prompt, config, startDir ?? workingDir);
|
|
31739
|
+
this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
|
|
31255
31740
|
logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
|
|
31256
31741
|
if (origin === "local") {
|
|
31257
31742
|
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
@@ -31265,11 +31750,37 @@ var WsClient = class {
|
|
|
31265
31750
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31266
31751
|
this.bus.startBusy(plan.cmd);
|
|
31267
31752
|
let seq = 0;
|
|
31753
|
+
let responseHeaderShown = false;
|
|
31754
|
+
let displayMode = "pending";
|
|
31755
|
+
let resultId = null;
|
|
31756
|
+
let resultBuf = "";
|
|
31268
31757
|
const result = await runAiFailover(plan, commandId, cwd2, {
|
|
31269
31758
|
onChunk: (text) => {
|
|
31270
|
-
|
|
31759
|
+
if (!responseHeaderShown) {
|
|
31760
|
+
this.bus.push({ source: origin, kind: "aires", text: `${plan.cmd} \u203A` });
|
|
31761
|
+
responseHeaderShown = true;
|
|
31762
|
+
}
|
|
31271
31763
|
appendCommandOutput(commandId, text);
|
|
31272
31764
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: text });
|
|
31765
|
+
if (displayMode === "result") {
|
|
31766
|
+
resultBuf += text;
|
|
31767
|
+
if (resultId) this.bus.updateEntry(resultId, resultBuf);
|
|
31768
|
+
return;
|
|
31769
|
+
}
|
|
31770
|
+
if (displayMode === "prose") {
|
|
31771
|
+
this.bus.push({ source: origin, kind: "out", text });
|
|
31772
|
+
return;
|
|
31773
|
+
}
|
|
31774
|
+
resultBuf += text;
|
|
31775
|
+
if (resultBuf.replace(/^\s+/, "").length === 0) return;
|
|
31776
|
+
const kind = looksLikeJsonOrCode(resultBuf);
|
|
31777
|
+
if (kind) {
|
|
31778
|
+
displayMode = "result";
|
|
31779
|
+
resultId = this.bus.push({ source: origin, kind: "result", text: resultBuf, resultKind: kind });
|
|
31780
|
+
} else {
|
|
31781
|
+
displayMode = "prose";
|
|
31782
|
+
this.bus.push({ source: origin, kind: "out", text: resultBuf });
|
|
31783
|
+
}
|
|
31273
31784
|
},
|
|
31274
31785
|
onAttemptStart: (label, index, total) => this.bus.push({
|
|
31275
31786
|
source: origin,
|
|
@@ -31324,6 +31835,79 @@ var WsClient = class {
|
|
|
31324
31835
|
level: result.exitCode === 0 ? "info" : "error"
|
|
31325
31836
|
});
|
|
31326
31837
|
}
|
|
31838
|
+
/**
|
|
31839
|
+
* Pick the Claude profile to try first when under session pressure (ADR-0081). Returns
|
|
31840
|
+
* the next candidate dir (cyclically after the current working one) when the knob is set
|
|
31841
|
+
* and the live session utilization is at/over it AND there is more than one candidate;
|
|
31842
|
+
* otherwise null (keep the normal working-first ordering). Claude-only (session % is a
|
|
31843
|
+
* Claude subscription metric).
|
|
31844
|
+
*/
|
|
31845
|
+
profileUnderSessionPressure(config, cmd, workingDir) {
|
|
31846
|
+
const threshold = config.sessionSwitchPct ?? 0;
|
|
31847
|
+
if (threshold <= 0 || cmd !== "claude") return null;
|
|
31848
|
+
const util2 = this.usageSnapshot?.session.utilizationPct ?? 0;
|
|
31849
|
+
if (util2 < threshold) return null;
|
|
31850
|
+
const dirs = claudeConfigDirs(config);
|
|
31851
|
+
if (dirs.length < 2) return null;
|
|
31852
|
+
const currentIndex = workingDir ? dirs.indexOf(workingDir) : -1;
|
|
31853
|
+
const next = dirs[(currentIndex + 1) % dirs.length];
|
|
31854
|
+
return next ?? null;
|
|
31855
|
+
}
|
|
31856
|
+
/**
|
|
31857
|
+
* Reject a prompt whose estimated tokens exceed the project's per-prompt limit (ADR-0081)
|
|
31858
|
+
* without spawning: surface it in the transcript + close the command on the server with a
|
|
31859
|
+
* PROMPT_TOKEN_LIMIT_EXCEEDED note (exit 1). No usage is metered (nothing ran).
|
|
31860
|
+
*/
|
|
31861
|
+
rejectPromptOverLimit(commandId, origin, estimated, limit) {
|
|
31862
|
+
const message = `[PROMPT_TOKEN_LIMIT_EXCEEDED] Prompt (~${estimated} tokens) exceeds the project per-prompt limit of ${limit} tokens.`;
|
|
31863
|
+
logger.warn("command.ai.prompt-limit", { commandId, origin, estimated, limit });
|
|
31864
|
+
this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
|
|
31865
|
+
if (origin === "local") {
|
|
31866
|
+
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
31867
|
+
commandId,
|
|
31868
|
+
cmd: "claude",
|
|
31869
|
+
args: [],
|
|
31870
|
+
origin: "local"
|
|
31871
|
+
});
|
|
31872
|
+
}
|
|
31873
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 0, chunk: message });
|
|
31874
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 1, chunk: "", done: true, exitCode: 1 });
|
|
31875
|
+
this.bus.push({
|
|
31876
|
+
source: origin,
|
|
31877
|
+
kind: "exit",
|
|
31878
|
+
text: "\u2717 prompt rejected \u2014 over the project per-prompt token limit",
|
|
31879
|
+
level: "error"
|
|
31880
|
+
});
|
|
31881
|
+
}
|
|
31882
|
+
/**
|
|
31883
|
+
* Reject a prompt an outbound reviewer blocked (ADR-0082) without spawning: surface the
|
|
31884
|
+
* verdict + close the command on the server. `reasons` contains only violation categories
|
|
31885
|
+
* (never secret values); a "unavailable" reason maps to no-outbound-available.
|
|
31886
|
+
*/
|
|
31887
|
+
rejectByReview(commandId, origin, reasons) {
|
|
31888
|
+
const unavailable = reasons.includes("unavailable");
|
|
31889
|
+
const code = unavailable ? "OUTBOUND_REVIEW_UNAVAILABLE" : "OUTBOUND_REVIEW_REJECTED";
|
|
31890
|
+
const detail = reasons.filter((r) => r !== "unavailable").join(", ");
|
|
31891
|
+
const message = unavailable ? `[${code}] No outbound reviewer is available \u2014 input blocked.` : `[${code}] Outbound review rejected the input${detail ? ` (${detail})` : ""}.`;
|
|
31892
|
+
logger.warn("command.ai.outbound-review", { commandId, origin, reasons });
|
|
31893
|
+
this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
|
|
31894
|
+
if (origin === "local") {
|
|
31895
|
+
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
31896
|
+
commandId,
|
|
31897
|
+
cmd: "claude",
|
|
31898
|
+
args: [],
|
|
31899
|
+
origin: "local"
|
|
31900
|
+
});
|
|
31901
|
+
}
|
|
31902
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 0, chunk: message });
|
|
31903
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: 1, chunk: "", done: true, exitCode: 1 });
|
|
31904
|
+
this.bus.push({
|
|
31905
|
+
source: origin,
|
|
31906
|
+
kind: "exit",
|
|
31907
|
+
text: "\u2717 input blocked by outbound review",
|
|
31908
|
+
level: "error"
|
|
31909
|
+
});
|
|
31910
|
+
}
|
|
31327
31911
|
/**
|
|
31328
31912
|
* Send one envelope (encrypted payload + per-message wsToken — arch 0004).
|
|
31329
31913
|
* When disconnected: the running command's output is held in a buffer (512KB cap,
|
|
@@ -31350,7 +31934,7 @@ var WsClient = class {
|
|
|
31350
31934
|
* Used for quota.check before spawning an AI cli (ADR-0020).
|
|
31351
31935
|
*/
|
|
31352
31936
|
request(channel, data) {
|
|
31353
|
-
return new Promise((
|
|
31937
|
+
return new Promise((resolve6, reject) => {
|
|
31354
31938
|
if (!this.socket || !this.sessionKey) {
|
|
31355
31939
|
reject(new Error("Not connected to the server."));
|
|
31356
31940
|
return;
|
|
@@ -31370,7 +31954,7 @@ var WsClient = class {
|
|
|
31370
31954
|
}, 15e3);
|
|
31371
31955
|
timer.unref?.();
|
|
31372
31956
|
this.pending.set(envelope.id, {
|
|
31373
|
-
resolve: (value) =>
|
|
31957
|
+
resolve: (value) => resolve6(value),
|
|
31374
31958
|
timer
|
|
31375
31959
|
});
|
|
31376
31960
|
this.socket.send(JSON.stringify(envelope));
|
|
@@ -31584,7 +32168,7 @@ var SessionBus = class {
|
|
|
31584
32168
|
get status() {
|
|
31585
32169
|
return this.currentStatus;
|
|
31586
32170
|
}
|
|
31587
|
-
/** Producer → consumers: push one transcript entry. */
|
|
32171
|
+
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
31588
32172
|
push(input) {
|
|
31589
32173
|
const entry = {
|
|
31590
32174
|
id: randomUUID2(),
|
|
@@ -31595,6 +32179,23 @@ var SessionBus = class {
|
|
|
31595
32179
|
this.history.push(entry);
|
|
31596
32180
|
if (this.history.length > REPLAY_CAP) this.history.shift();
|
|
31597
32181
|
this.emitter.emit("transcript", entry);
|
|
32182
|
+
return entry.id;
|
|
32183
|
+
}
|
|
32184
|
+
/**
|
|
32185
|
+
* Producer → consumers: replace the text of an already-pushed entry (ADR-0108) — used to
|
|
32186
|
+
* grow a streaming `result` block in place without pushing a new line per chunk. No-op if the
|
|
32187
|
+
* entry has aged out of the replay window.
|
|
32188
|
+
*/
|
|
32189
|
+
updateEntry(id, text) {
|
|
32190
|
+
const entry = this.history.find((e) => e.id === id);
|
|
32191
|
+
if (!entry) return;
|
|
32192
|
+
entry.text = text;
|
|
32193
|
+
this.emitter.emit("transcript-update", entry);
|
|
32194
|
+
}
|
|
32195
|
+
/** Subscribe to in-place entry updates (a `result` block growing — ADR-0108). */
|
|
32196
|
+
onTranscriptUpdate(fn) {
|
|
32197
|
+
this.emitter.on("transcript-update", fn);
|
|
32198
|
+
return () => this.emitter.off("transcript-update", fn);
|
|
31598
32199
|
}
|
|
31599
32200
|
/** Shortcut for a lifecycle log line (source = system). */
|
|
31600
32201
|
log(text, level = "info") {
|
|
@@ -33699,7 +34300,7 @@ var src_default = Yoga;
|
|
|
33699
34300
|
|
|
33700
34301
|
// ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
|
|
33701
34302
|
var import_react_reconciler = __toESM(require_react_reconciler(), 1);
|
|
33702
|
-
var
|
|
34303
|
+
var import_constants10 = __toESM(require_constants(), 1);
|
|
33703
34304
|
import process3 from "process";
|
|
33704
34305
|
|
|
33705
34306
|
// ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
@@ -34941,7 +35542,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
|
|
|
34941
35542
|
scheduleTimeout: setTimeout,
|
|
34942
35543
|
cancelTimeout: clearTimeout,
|
|
34943
35544
|
noTimeout: -1,
|
|
34944
|
-
getCurrentEventPriority: () =>
|
|
35545
|
+
getCurrentEventPriority: () => import_constants10.DefaultEventPriority,
|
|
34945
35546
|
beforeActiveInstanceBlur() {
|
|
34946
35547
|
},
|
|
34947
35548
|
afterActiveInstanceBlur() {
|
|
@@ -37015,8 +37616,8 @@ var Ink = class {
|
|
|
37015
37616
|
}
|
|
37016
37617
|
}
|
|
37017
37618
|
async waitUntilExit() {
|
|
37018
|
-
this.exitPromise ||= new Promise((
|
|
37019
|
-
this.resolveExitPromise =
|
|
37619
|
+
this.exitPromise ||= new Promise((resolve6, reject) => {
|
|
37620
|
+
this.resolveExitPromise = resolve6;
|
|
37020
37621
|
this.rejectExitPromise = reject;
|
|
37021
37622
|
});
|
|
37022
37623
|
return this.exitPromise;
|
|
@@ -37513,6 +38114,16 @@ function textColor(line) {
|
|
|
37513
38114
|
if (line.kind === "exit") return "green";
|
|
37514
38115
|
return void 0;
|
|
37515
38116
|
}
|
|
38117
|
+
function AiMarkerLine({ line }) {
|
|
38118
|
+
const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
|
|
38119
|
+
const idx = line.text.indexOf(arrow);
|
|
38120
|
+
if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
|
|
38121
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
|
|
38122
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: line.text.slice(0, idx) }),
|
|
38123
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
|
|
38124
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
|
|
38125
|
+
] });
|
|
38126
|
+
}
|
|
37516
38127
|
function wrapToWidth(text, width) {
|
|
37517
38128
|
if (width <= 0 || text.length <= width) return [text];
|
|
37518
38129
|
const rows = [];
|
|
@@ -37538,20 +38149,94 @@ function wrapToWidth(text, width) {
|
|
|
37538
38149
|
if (line) rows.push(line.trimEnd());
|
|
37539
38150
|
return rows.length > 0 ? rows : [""];
|
|
37540
38151
|
}
|
|
37541
|
-
|
|
38152
|
+
var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
|
|
38153
|
+
function toSegments(raw) {
|
|
38154
|
+
if (!raw.includes("<detail")) {
|
|
38155
|
+
return raw.split("\n").map((text) => ({ text, collapsed: false }));
|
|
38156
|
+
}
|
|
38157
|
+
const out = [];
|
|
38158
|
+
const re = new RegExp(DETAIL_RE);
|
|
38159
|
+
let last = 0;
|
|
38160
|
+
let m;
|
|
38161
|
+
while ((m = re.exec(raw)) !== null) {
|
|
38162
|
+
const before = raw.slice(last, m.index).replace(/\n$/, "");
|
|
38163
|
+
if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
|
|
38164
|
+
out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
|
|
38165
|
+
last = m.index + m[0].length;
|
|
38166
|
+
if (raw[last] === "\n") last += 1;
|
|
38167
|
+
}
|
|
38168
|
+
const rest = raw.slice(last);
|
|
38169
|
+
if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
|
|
38170
|
+
if (out.length === 0) out.push({ text: "", collapsed: false });
|
|
38171
|
+
return out;
|
|
38172
|
+
}
|
|
38173
|
+
function pushCollapsible(lines, entry, opts) {
|
|
38174
|
+
const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
|
|
38175
|
+
const base = { source: entry.source, level: entry.level, kind: entry.kind };
|
|
38176
|
+
lines.push({
|
|
38177
|
+
...base,
|
|
38178
|
+
key: `${keyBase}:m`,
|
|
38179
|
+
text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
|
|
38180
|
+
showTag: firstRow,
|
|
38181
|
+
collapsed: true,
|
|
38182
|
+
spacer: false,
|
|
38183
|
+
blockIndex: blockNo
|
|
38184
|
+
});
|
|
38185
|
+
if (!expanded) return;
|
|
38186
|
+
prettyResult(body).split("\n").forEach(
|
|
38187
|
+
(seg, i) => wrapToWidth(seg, textWidth).forEach(
|
|
38188
|
+
(row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
|
|
38189
|
+
)
|
|
38190
|
+
);
|
|
38191
|
+
}
|
|
38192
|
+
function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
|
|
37542
38193
|
const lines = [];
|
|
38194
|
+
let prevKey = null;
|
|
38195
|
+
let blockNo = 0;
|
|
37543
38196
|
for (const entry of entries2) {
|
|
38197
|
+
const blockKey = `${entry.source}|${entry.kind}`;
|
|
38198
|
+
const newBlock = blockKey !== prevKey;
|
|
38199
|
+
if (newBlock && lines.length > 0) {
|
|
38200
|
+
lines.push({
|
|
38201
|
+
key: `sp:${entry.id}`,
|
|
38202
|
+
source: entry.source,
|
|
38203
|
+
level: entry.level,
|
|
38204
|
+
kind: entry.kind,
|
|
38205
|
+
text: "",
|
|
38206
|
+
showTag: false,
|
|
38207
|
+
collapsed: false,
|
|
38208
|
+
spacer: true
|
|
38209
|
+
});
|
|
38210
|
+
}
|
|
38211
|
+
prevKey = blockKey;
|
|
38212
|
+
let firstRow = newBlock;
|
|
38213
|
+
if (entry.kind === "result") {
|
|
38214
|
+
blockNo += 1;
|
|
38215
|
+
const count = prettyResult(entry.text).split("\n").length;
|
|
38216
|
+
const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
|
|
38217
|
+
pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
|
|
38218
|
+
continue;
|
|
38219
|
+
}
|
|
37544
38220
|
const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
|
|
37545
|
-
|
|
37546
|
-
|
|
37547
|
-
|
|
38221
|
+
toSegments(raw).forEach((seg, i) => {
|
|
38222
|
+
if (seg.collapsed) {
|
|
38223
|
+
blockNo += 1;
|
|
38224
|
+
pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
|
|
38225
|
+
firstRow = false;
|
|
38226
|
+
return;
|
|
38227
|
+
}
|
|
38228
|
+
wrapToWidth(seg.text, textWidth).forEach((row, j) => {
|
|
37548
38229
|
lines.push({
|
|
37549
38230
|
key: `${entry.id}:${i}:${j}`,
|
|
37550
38231
|
source: entry.source,
|
|
37551
38232
|
level: entry.level,
|
|
37552
38233
|
kind: entry.kind,
|
|
37553
|
-
text: row
|
|
38234
|
+
text: row,
|
|
38235
|
+
showTag: firstRow,
|
|
38236
|
+
collapsed: false,
|
|
38237
|
+
spacer: false
|
|
37554
38238
|
});
|
|
38239
|
+
firstRow = false;
|
|
37555
38240
|
});
|
|
37556
38241
|
});
|
|
37557
38242
|
}
|
|
@@ -37560,8 +38245,8 @@ function flattenEntries(entries2, textWidth) {
|
|
|
37560
38245
|
function TranscriptLine({ line }) {
|
|
37561
38246
|
const tag = sourceTag(line.source);
|
|
37562
38247
|
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 || " " }) })
|
|
38248
|
+
/* @__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 }),
|
|
38249
|
+
/* @__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
38250
|
] });
|
|
37566
38251
|
}
|
|
37567
38252
|
|
|
@@ -37758,6 +38443,19 @@ function runWhoami(ctx) {
|
|
|
37758
38443
|
ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
|
|
37759
38444
|
});
|
|
37760
38445
|
}
|
|
38446
|
+
function runFold(ctx, action) {
|
|
38447
|
+
if (ctx.maxBlock === 0) {
|
|
38448
|
+
ctx.print("No collapsible blocks on screen.", "warn");
|
|
38449
|
+
return;
|
|
38450
|
+
}
|
|
38451
|
+
const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
|
|
38452
|
+
if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
|
|
38453
|
+
ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
|
|
38454
|
+
return;
|
|
38455
|
+
}
|
|
38456
|
+
if (action === "expand") ctx.expand(n);
|
|
38457
|
+
else ctx.collapse(n);
|
|
38458
|
+
}
|
|
37761
38459
|
var COMMANDS = [
|
|
37762
38460
|
{
|
|
37763
38461
|
name: "help",
|
|
@@ -37810,6 +38508,18 @@ var COMMANDS = [
|
|
|
37810
38508
|
description: "Tail the profile's structured log",
|
|
37811
38509
|
run: runLogs
|
|
37812
38510
|
},
|
|
38511
|
+
{
|
|
38512
|
+
name: "expand",
|
|
38513
|
+
usage: "/expand [N]",
|
|
38514
|
+
description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
|
|
38515
|
+
run: (ctx) => runFold(ctx, "expand")
|
|
38516
|
+
},
|
|
38517
|
+
{
|
|
38518
|
+
name: "collapse",
|
|
38519
|
+
usage: "/collapse [N]",
|
|
38520
|
+
description: "Collapse an expanded \u25B8[N] block (default: newest)",
|
|
38521
|
+
run: (ctx) => runFold(ctx, "collapse")
|
|
38522
|
+
},
|
|
37813
38523
|
{
|
|
37814
38524
|
name: "reconnect",
|
|
37815
38525
|
usage: "/reconnect",
|
|
@@ -38016,13 +38726,18 @@ function App2({
|
|
|
38016
38726
|
const [usage, setUsage] = (0, import_react26.useState)(bus.usage);
|
|
38017
38727
|
const [sessionTokens, setSessionTokens] = (0, import_react26.useState)(bus.sessionTokens);
|
|
38018
38728
|
const [scrollBack, setScrollBack] = (0, import_react26.useState)(0);
|
|
38729
|
+
const [expanded, setExpanded] = (0, import_react26.useState)(() => /* @__PURE__ */ new Set());
|
|
38019
38730
|
const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
|
|
38020
38731
|
const [startedAt] = (0, import_react26.useState)(() => Date.now());
|
|
38021
38732
|
(0, import_react26.useEffect)(() => {
|
|
38022
|
-
const offTranscript = bus.onTranscript(
|
|
38023
|
-
|
|
38733
|
+
const offTranscript = bus.onTranscript((entry) => {
|
|
38734
|
+
setEntries(
|
|
38024
38735
|
(prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
|
|
38025
|
-
)
|
|
38736
|
+
);
|
|
38737
|
+
if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
|
|
38738
|
+
});
|
|
38739
|
+
const offUpdate = bus.onTranscriptUpdate(
|
|
38740
|
+
(updated) => setEntries((prev) => prev.map((e) => e.id === updated.id ? { ...e, text: updated.text } : e))
|
|
38026
38741
|
);
|
|
38027
38742
|
const offStatus = bus.onStatus(setStatus);
|
|
38028
38743
|
const offBusy = bus.onBusy(setBusy);
|
|
@@ -38034,6 +38749,7 @@ function App2({
|
|
|
38034
38749
|
const offTokens = bus.onTokens(setSessionTokens);
|
|
38035
38750
|
return () => {
|
|
38036
38751
|
offTranscript();
|
|
38752
|
+
offUpdate();
|
|
38037
38753
|
offStatus();
|
|
38038
38754
|
offBusy();
|
|
38039
38755
|
offProfile();
|
|
@@ -38044,6 +38760,29 @@ function App2({
|
|
|
38044
38760
|
offTokens();
|
|
38045
38761
|
};
|
|
38046
38762
|
}, [bus]);
|
|
38763
|
+
const bodyHeight = Math.max(3, rows - CHROME_ROWS);
|
|
38764
|
+
const textWidth = Math.max(10, cols - 1 - 8);
|
|
38765
|
+
const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
|
|
38766
|
+
const maxBlock = (0, import_react26.useMemo)(() => lines.reduce((m, l) => Math.max(m, l.blockIndex ?? 0), 0), [lines]);
|
|
38767
|
+
const expandFold = (n) => {
|
|
38768
|
+
const target = n ?? maxBlock;
|
|
38769
|
+
if (target < 1 || target > maxBlock) return;
|
|
38770
|
+
setExpanded((prev) => {
|
|
38771
|
+
const next = new Set(prev);
|
|
38772
|
+
if (next.has(target)) next.delete(target);
|
|
38773
|
+
else next.add(target);
|
|
38774
|
+
return next;
|
|
38775
|
+
});
|
|
38776
|
+
};
|
|
38777
|
+
const collapseFold = (n) => {
|
|
38778
|
+
const target = n ?? maxBlock;
|
|
38779
|
+
setExpanded((prev) => {
|
|
38780
|
+
if (!prev.has(target)) return prev;
|
|
38781
|
+
const next = new Set(prev);
|
|
38782
|
+
next.delete(target);
|
|
38783
|
+
return next;
|
|
38784
|
+
});
|
|
38785
|
+
};
|
|
38047
38786
|
const handleSubmit = (input) => {
|
|
38048
38787
|
setScrollBack(0);
|
|
38049
38788
|
setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
|
|
@@ -38061,15 +38800,15 @@ function App2({
|
|
|
38061
38800
|
quit: () => exit(),
|
|
38062
38801
|
submitAi: (aiInput) => bus.submitLocal(aiInput),
|
|
38063
38802
|
reconnect: () => bus.requestReconnect(),
|
|
38803
|
+
expand: expandFold,
|
|
38804
|
+
collapse: collapseFold,
|
|
38805
|
+
maxBlock,
|
|
38064
38806
|
live: { status, scope, worker, project, activeProfile, startedAt }
|
|
38065
38807
|
});
|
|
38066
38808
|
return;
|
|
38067
38809
|
}
|
|
38068
38810
|
bus.submitLocal(input);
|
|
38069
38811
|
};
|
|
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
38812
|
const maxScroll = Math.max(0, lines.length - bodyHeight);
|
|
38074
38813
|
const clampedBack = Math.min(scrollBack, maxScroll);
|
|
38075
38814
|
const lineRows = clampedBack > 0 ? bodyHeight - 1 : bodyHeight;
|
|
@@ -38172,6 +38911,20 @@ async function runStart(profileDir2, profileName) {
|
|
|
38172
38911
|
profileDir: profileDir2,
|
|
38173
38912
|
bus
|
|
38174
38913
|
});
|
|
38914
|
+
const runResilient = async () => {
|
|
38915
|
+
for (; ; ) {
|
|
38916
|
+
try {
|
|
38917
|
+
await client.run();
|
|
38918
|
+
return;
|
|
38919
|
+
} catch (err) {
|
|
38920
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
38921
|
+
logger.error("ws.run.crash", { message: e.message, stack: e.stack });
|
|
38922
|
+
if (client.isStopped) return;
|
|
38923
|
+
bus.log(`Recovered from an internal error \u2014 reconnecting: ${e.message}`, "error");
|
|
38924
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
38925
|
+
}
|
|
38926
|
+
}
|
|
38927
|
+
};
|
|
38175
38928
|
const useTui = Boolean(process.stdout.isTTY) && process.env.FOURPM_HEADLESS !== "1";
|
|
38176
38929
|
if (useTui) {
|
|
38177
38930
|
const info = {
|
|
@@ -38190,12 +38943,12 @@ async function runStart(profileDir2, profileName) {
|
|
|
38190
38943
|
bus.setActiveProfile(workingDir ? profileDisplayLabel(workingDir) : labels[0] ?? null);
|
|
38191
38944
|
const tui = runTui(bus, info);
|
|
38192
38945
|
bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
|
|
38193
|
-
void
|
|
38946
|
+
void runResilient();
|
|
38194
38947
|
await tui;
|
|
38195
38948
|
} else {
|
|
38196
38949
|
attachConsoleSink(bus);
|
|
38197
38950
|
bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
|
|
38198
|
-
await
|
|
38951
|
+
await runResilient();
|
|
38199
38952
|
}
|
|
38200
38953
|
} finally {
|
|
38201
38954
|
release();
|
|
@@ -38225,7 +38978,7 @@ async function runUnlink(profileDir2, profileName) {
|
|
|
38225
38978
|
|
|
38226
38979
|
// src/commands/update.ts
|
|
38227
38980
|
async function runUpdate(profileDir2, profileName, serverUrlFlag) {
|
|
38228
|
-
const serverUrl = (serverUrlFlag
|
|
38981
|
+
const serverUrl = (serverUrlFlag || readCredential(profileDir2, profileName)?.serverUrl || process.env.FOURPM_SERVER || "http://localhost:42001").replace(/\/$/, "");
|
|
38229
38982
|
let result;
|
|
38230
38983
|
try {
|
|
38231
38984
|
result = await updateToLatest(serverUrl);
|
|
@@ -38295,14 +39048,21 @@ function Picker({
|
|
|
38295
39048
|
] });
|
|
38296
39049
|
}
|
|
38297
39050
|
function selectFromList(items, title) {
|
|
38298
|
-
return new Promise((
|
|
39051
|
+
return new Promise((resolve6) => {
|
|
38299
39052
|
let chosen = null;
|
|
38300
39053
|
const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
|
|
38301
|
-
void app.waitUntilExit().then(() =>
|
|
39054
|
+
void app.waitUntilExit().then(() => resolve6(chosen));
|
|
38302
39055
|
});
|
|
38303
39056
|
}
|
|
38304
39057
|
|
|
38305
39058
|
// src/index.ts
|
|
39059
|
+
process.on("unhandledRejection", (reason) => {
|
|
39060
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
39061
|
+
logger.error("process.unhandledRejection", { message: err.message, stack: err.stack });
|
|
39062
|
+
});
|
|
39063
|
+
process.on("uncaughtException", (err) => {
|
|
39064
|
+
logger.error("process.uncaughtException", { message: err.message, stack: err.stack });
|
|
39065
|
+
});
|
|
38306
39066
|
function flagValue(args, name) {
|
|
38307
39067
|
const index = args.indexOf(`--${name}`);
|
|
38308
39068
|
return index >= 0 ? args[index + 1] : void 0;
|
|
@@ -38331,7 +39091,7 @@ async function main() {
|
|
|
38331
39091
|
const [command, ...rest] = args;
|
|
38332
39092
|
switch (command) {
|
|
38333
39093
|
case "link": {
|
|
38334
|
-
const serverUrl = flagValue(rest, "server")
|
|
39094
|
+
const serverUrl = flagValue(rest, "server") || process.env.FOURPM_SERVER || "http://localhost:42001";
|
|
38335
39095
|
await runLink(serverUrl.replace(/\/$/, ""), explicitProfile);
|
|
38336
39096
|
break;
|
|
38337
39097
|
}
|
|
@@ -38380,7 +39140,7 @@ async function main() {
|
|
|
38380
39140
|
"",
|
|
38381
39141
|
"Profiles: named after the paired account (e.g. mcacc1); when several exist,",
|
|
38382
39142
|
"start/unlink show a picker (\u2191\u2193 + Enter) \u2014 no --profile needed.",
|
|
38383
|
-
"Server URL: from FOURPM_SERVER (default http://localhost:
|
|
39143
|
+
"Server URL: from FOURPM_SERVER (default http://localhost:42001)."
|
|
38384
39144
|
].join("\n")
|
|
38385
39145
|
);
|
|
38386
39146
|
}
|