@indigoai-us/hq-cli 5.345.153 → 5.345.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.154] — 2026-10-21
|
|
6
|
+
- `hq sync status` now lists local files that sit under a vault prefix you can
|
|
7
|
+
only read and that were never synced. Sync will never upload these files, for
|
|
8
|
+
example notes written before the vault write-access hook blocked that folder.
|
|
9
|
+
The list comes from the local `.hq/vault-access.json` access data and the
|
|
10
|
+
sync journal, so the command makes no extra network calls.
|
|
11
|
+
- `hq lanes resume` and `hq lanes message` without `--account` keep a Claude lane on the account it was created with via `--account`, instead of rebalancing it onto another account (which could belong to another company). Repair of an unusable account still moves the lane.
|
|
12
|
+
|
|
5
13
|
## [5.345.153] — 2026-10-21
|
|
6
14
|
- `hq-auth-refresh --min-validity-seconds N` now applies to person sessions,
|
|
7
15
|
not only machine tokens: a person session with less than N seconds left
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -401,4 +401,21 @@ export declare function registerCloudCommands(program: Command): void;
|
|
|
401
401
|
export declare function runSyncDoctorReconcileConflicts(hqRoot: string, yes: boolean): Promise<void>;
|
|
402
402
|
/** Exported for the US-003 sync-health exit-path regression test. */
|
|
403
403
|
export declare function runPullPersonal(hqRoot: string, onConflict?: ConflictStrategy): Promise<void>;
|
|
404
|
+
/**
|
|
405
|
+
* Human-friendly label for a journal slug in `hq sync status`. The
|
|
406
|
+
* personal-vault fanout slot and the legacy `companies/personal` company both
|
|
407
|
+
* carry opaque slugs; everything else is a real company slug shown as-is.
|
|
408
|
+
*/
|
|
409
|
+
/**
|
|
410
|
+
* `hq sync status`: list local files under read-only vault prefixes that are
|
|
411
|
+
* not in the sync journal. Push never uploads them (feedback_510661bc,
|
|
412
|
+
* Shepherd #2813). Uses only the local vault-access manifest and the journals
|
|
413
|
+
* already loaded, with no network calls. Prints nothing when nothing applies.
|
|
414
|
+
*/
|
|
415
|
+
export declare function printReadOnlyLocalOnly(hqRoot: string, journals: ReadonlyArray<{
|
|
416
|
+
slug: string;
|
|
417
|
+
journal: {
|
|
418
|
+
files?: Record<string, unknown>;
|
|
419
|
+
};
|
|
420
|
+
}>, log?: (line: string) => void): number;
|
|
404
421
|
//# sourceMappingURL=cloud.d.ts.map
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
import { isAccessDeniedError, accessDeniedKeyOf, formatAccessDenied, } from "../utils/access-denied-hint.js";
|
|
16
|
+
import { findReadOnlyLocalOnly, isGrantEnforced, readVaultAccessManifest, } from "../lib/sync/read-only-local-only.js";
|
|
16
17
|
import chalk from "chalk";
|
|
17
18
|
import * as fs from "fs";
|
|
18
19
|
import * as path from "path";
|
|
19
|
-
import { share, getStateDir, listJournals, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
20
|
+
import { share, getStateDir, listJournals, createIgnoreFilter, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
20
21
|
import { syncLeg } from "../utils/sync-operation-lock.js";
|
|
21
22
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
22
23
|
import { resolveUploadAuthor } from "../utils/upload-author.js";
|
|
@@ -1353,6 +1354,7 @@ export function registerCloudCommands(program) {
|
|
|
1353
1354
|
console.log(` Total size: ${formatBytes(grandBytes)}`);
|
|
1354
1355
|
console.log(` Last sync: ${grandLastSync || "never"}`);
|
|
1355
1356
|
}
|
|
1357
|
+
printReadOnlyLocalOnly(hqRoot, journals);
|
|
1356
1358
|
}
|
|
1357
1359
|
catch (err) {
|
|
1358
1360
|
console.error(chalk.red("✗ Status failed:"), err instanceof Error ? err.message : String(err));
|
|
@@ -2094,6 +2096,51 @@ function readActiveCompanySlug(hqRoot) {
|
|
|
2094
2096
|
* personal-vault fanout slot and the legacy `companies/personal` company both
|
|
2095
2097
|
* carry opaque slugs; everything else is a real company slug shown as-is.
|
|
2096
2098
|
*/
|
|
2099
|
+
/**
|
|
2100
|
+
* `hq sync status`: list local files under read-only vault prefixes that are
|
|
2101
|
+
* not in the sync journal. Push never uploads them (feedback_510661bc,
|
|
2102
|
+
* Shepherd #2813). Uses only the local vault-access manifest and the journals
|
|
2103
|
+
* already loaded, with no network calls. Prints nothing when nothing applies.
|
|
2104
|
+
*/
|
|
2105
|
+
export function printReadOnlyLocalOnly(hqRoot, journals, log = (line) => console.log(line)) {
|
|
2106
|
+
const companies = readVaultAccessManifest(hqRoot)?.companies;
|
|
2107
|
+
if (!companies)
|
|
2108
|
+
return 0;
|
|
2109
|
+
let shouldSync;
|
|
2110
|
+
let grand = 0;
|
|
2111
|
+
for (const [slug, company] of Object.entries(companies)) {
|
|
2112
|
+
if (!isGrantEnforced(company))
|
|
2113
|
+
continue;
|
|
2114
|
+
if (!fs.existsSync(path.join(hqRoot, "companies", slug)))
|
|
2115
|
+
continue;
|
|
2116
|
+
if (!shouldSync) {
|
|
2117
|
+
try {
|
|
2118
|
+
shouldSync = createIgnoreFilter(hqRoot);
|
|
2119
|
+
}
|
|
2120
|
+
catch {
|
|
2121
|
+
shouldSync = () => true;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
const journal = journals.find((j) => j.slug === slug)?.journal;
|
|
2125
|
+
const journalKeys = new Set(Object.keys(journal?.files ?? {}));
|
|
2126
|
+
const found = findReadOnlyLocalOnly({ hqRoot, slug, company, journalKeys, shouldSync });
|
|
2127
|
+
if (found.total === 0)
|
|
2128
|
+
continue;
|
|
2129
|
+
grand += found.total;
|
|
2130
|
+
log(chalk.yellow(`\n ! ${slug}: ${found.total} local-only file(s) under read-only access will not upload`));
|
|
2131
|
+
for (const f of found.files) {
|
|
2132
|
+
log(` ${f.path}${f.permission === "none" ? chalk.dim(" (no access)") : ""}`);
|
|
2133
|
+
}
|
|
2134
|
+
if (found.total > found.files.length) {
|
|
2135
|
+
log(chalk.dim(` ... and ${found.total - found.files.length} more`));
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (grand > 0) {
|
|
2139
|
+
log(chalk.dim(" Copy them somewhere you can write, or ask a company admin for write access. " +
|
|
2140
|
+
"Access data: .hq/vault-access.json (refresh: bash core/scripts/refresh-vault-access.sh)."));
|
|
2141
|
+
}
|
|
2142
|
+
return grand;
|
|
2143
|
+
}
|
|
2097
2144
|
function scopeLabel(slug) {
|
|
2098
2145
|
if (slug === PERSONAL_VAULT_JOURNAL_SLUG)
|
|
2099
2146
|
return "personal vault";
|
package/dist/commands/lanes.js
CHANGED
|
@@ -7801,7 +7801,13 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
7801
7801
|
accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
|
|
7802
7802
|
providerModel: config.providers.claude.model,
|
|
7803
7803
|
});
|
|
7804
|
-
|
|
7804
|
+
// An account the operator chose with --account (account_source "cli") is a
|
|
7805
|
+
// pin, not a balancing hint. Plain resumes and messages keep it; only a repair
|
|
7806
|
+
// of an unusable account may move the lane, and only an explicit --account
|
|
7807
|
+
// may pick another healthy account. Rebalancing a pinned lane moved company
|
|
7808
|
+
// lanes onto another company's account (feedback_4a563e61).
|
|
7809
|
+
const cliAccountPin = lane.account !== undefined && lane.account_source === "cli";
|
|
7810
|
+
const mayRebalance = !cliAccountPin && BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
7805
7811
|
if (!accountNeedsRepair && !mayRebalance)
|
|
7806
7812
|
return undefined;
|
|
7807
7813
|
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type VaultGrant = {
|
|
2
|
+
path: string;
|
|
3
|
+
permission: string;
|
|
4
|
+
};
|
|
5
|
+
export type VaultAccessCompany = {
|
|
6
|
+
role?: string;
|
|
7
|
+
enforced?: boolean;
|
|
8
|
+
grants?: VaultGrant[];
|
|
9
|
+
};
|
|
10
|
+
export type VaultAccessManifest = {
|
|
11
|
+
version?: number;
|
|
12
|
+
companies?: Record<string, VaultAccessCompany>;
|
|
13
|
+
};
|
|
14
|
+
/** bypass | unknown | admin | write | read | none */
|
|
15
|
+
export type EffectivePermission = "bypass" | "unknown" | "admin" | "write" | "read" | "none";
|
|
16
|
+
export declare function readVaultAccessManifest(hqRoot: string): VaultAccessManifest | null;
|
|
17
|
+
/** Whether the manifest entry is one the write hook enforces grants for. */
|
|
18
|
+
export declare function isGrantEnforced(company: VaultAccessCompany | undefined): boolean;
|
|
19
|
+
export declare function effectivePermission(company: VaultAccessCompany | undefined, rel: string): EffectivePermission;
|
|
20
|
+
export type ReadOnlyLocalOnlyFile = {
|
|
21
|
+
path: string;
|
|
22
|
+
permission: "read" | "none";
|
|
23
|
+
};
|
|
24
|
+
export type ReadOnlyLocalOnlyResult = {
|
|
25
|
+
files: ReadOnlyLocalOnlyFile[];
|
|
26
|
+
total: number;
|
|
27
|
+
};
|
|
28
|
+
export type FindReadOnlyLocalOnlyOptions = {
|
|
29
|
+
hqRoot: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
company: VaultAccessCompany;
|
|
32
|
+
/** Company-relative keys already in the sync journal (live or tombstoned). */
|
|
33
|
+
journalKeys: ReadonlySet<string>;
|
|
34
|
+
/** Engine ignore filter over absolute paths; true = would sync. */
|
|
35
|
+
shouldSync?: (absPath: string, isDir?: boolean) => boolean;
|
|
36
|
+
/** Max paths returned; `total` still counts every match. */
|
|
37
|
+
limit?: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Walk `companies/<slug>/` and return files that are not in the journal and
|
|
41
|
+
* whose effective permission is read or none. Returns an empty result for
|
|
42
|
+
* roles the hook does not enforce (owner/admin/unknown, enforced=false).
|
|
43
|
+
*/
|
|
44
|
+
export declare function findReadOnlyLocalOnly(opts: FindReadOnlyLocalOnlyOptions): ReadOnlyLocalOnlyResult;
|
|
45
|
+
//# sourceMappingURL=read-only-local-only.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-only files under read-only vault prefixes (feedback_510661bc, Shepherd #2813).
|
|
3
|
+
*
|
|
4
|
+
* A teammate with a read-only share can still hold local files under that
|
|
5
|
+
* prefix — for example files written before the enforce-vault-write-access
|
|
6
|
+
* hook existed. Push will never upload them, so `hq sync status` lists them.
|
|
7
|
+
*
|
|
8
|
+
* Read-only and network-free: the access data is the local manifest at
|
|
9
|
+
* `<hqRoot>/.hq/vault-access.json` (written by refresh-vault-access), and the
|
|
10
|
+
* "already synced" set is the company's sync journal the caller already has.
|
|
11
|
+
* Permission resolution mirrors `effective_permission` in
|
|
12
|
+
* `.claude/hooks/enforce-vault-write-access.sh`.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
export function readVaultAccessManifest(hqRoot) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq", "vault-access.json"), "utf-8"));
|
|
19
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function rank(permission) {
|
|
26
|
+
if (permission === "admin")
|
|
27
|
+
return 3;
|
|
28
|
+
if (permission === "write")
|
|
29
|
+
return 2;
|
|
30
|
+
if (permission === "read")
|
|
31
|
+
return 1;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
function grantMatches(grant, rel) {
|
|
35
|
+
const p = grant.path;
|
|
36
|
+
if (typeof p !== "string")
|
|
37
|
+
return false;
|
|
38
|
+
if (p === "*" || p === rel)
|
|
39
|
+
return true;
|
|
40
|
+
if (p.endsWith("/*")) {
|
|
41
|
+
return rel.startsWith(p.slice(0, -1)) || rel === p.slice(0, -2);
|
|
42
|
+
}
|
|
43
|
+
if (p.endsWith("/") && (grant.permission === "write" || grant.permission === "admin") && rel.startsWith(p)) {
|
|
44
|
+
const child = rel.slice(p.length);
|
|
45
|
+
return child.length > 0 && !child.includes("/");
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/** Whether the manifest entry is one the write hook enforces grants for. */
|
|
50
|
+
export function isGrantEnforced(company) {
|
|
51
|
+
if (!company)
|
|
52
|
+
return false;
|
|
53
|
+
const role = company.role ?? "unknown";
|
|
54
|
+
if (role !== "member" && role !== "guest")
|
|
55
|
+
return false;
|
|
56
|
+
return company.enforced !== false;
|
|
57
|
+
}
|
|
58
|
+
export function effectivePermission(company, rel) {
|
|
59
|
+
if (!company)
|
|
60
|
+
return "unknown";
|
|
61
|
+
const role = company.role ?? "unknown";
|
|
62
|
+
if (role === "owner" || role === "admin")
|
|
63
|
+
return "bypass";
|
|
64
|
+
if (company.enforced === false)
|
|
65
|
+
return "bypass";
|
|
66
|
+
if (role !== "member" && role !== "guest")
|
|
67
|
+
return "unknown";
|
|
68
|
+
const matches = (company.grants ?? []).filter((g) => g && grantMatches(g, rel));
|
|
69
|
+
if (matches.length === 0)
|
|
70
|
+
return "none";
|
|
71
|
+
matches.sort((a, b) => a.path.length - b.path.length || rank(a.permission) - rank(b.permission));
|
|
72
|
+
const best = matches[matches.length - 1].permission;
|
|
73
|
+
return best === "admin" || best === "write" || best === "read" ? best : "none";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Walk `companies/<slug>/` and return files that are not in the journal and
|
|
77
|
+
* whose effective permission is read or none. Returns an empty result for
|
|
78
|
+
* roles the hook does not enforce (owner/admin/unknown, enforced=false).
|
|
79
|
+
*/
|
|
80
|
+
export function findReadOnlyLocalOnly(opts) {
|
|
81
|
+
if (!isGrantEnforced(opts.company))
|
|
82
|
+
return { files: [], total: 0 };
|
|
83
|
+
const found = [];
|
|
84
|
+
const root = path.join(opts.hqRoot, "companies", opts.slug);
|
|
85
|
+
const limit = opts.limit ?? 20;
|
|
86
|
+
const stack = [root];
|
|
87
|
+
while (stack.length > 0) {
|
|
88
|
+
const dir = stack.pop();
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const abs = path.join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
100
|
+
continue;
|
|
101
|
+
if (opts.shouldSync && !opts.shouldSync(abs, true))
|
|
102
|
+
continue;
|
|
103
|
+
stack.push(abs);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!entry.isFile())
|
|
107
|
+
continue;
|
|
108
|
+
if (opts.shouldSync && !opts.shouldSync(abs, false))
|
|
109
|
+
continue;
|
|
110
|
+
const rel = path.relative(root, abs).split(path.sep).join("/");
|
|
111
|
+
if (opts.journalKeys.has(rel))
|
|
112
|
+
continue;
|
|
113
|
+
const permission = effectivePermission(opts.company, rel);
|
|
114
|
+
if (permission !== "read" && permission !== "none")
|
|
115
|
+
continue;
|
|
116
|
+
found.push({ path: rel, permission });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
found.sort((a, b) => a.path.localeCompare(b.path));
|
|
120
|
+
return { files: found.slice(0, limit), total: found.length };
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=read-only-local-only.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.154",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.154",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.154",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.154",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.154",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.154",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.154"
|
|
123
123
|
}
|
|
124
124
|
}
|