nolo-cli 0.1.41 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/agent-runtime/agentThread.ts +222 -0
- package/agent-runtime/agentThreadAdmission.ts +74 -0
- package/agent-runtime/dialogWritePlan.ts +42 -0
- package/agent-runtime/hostAdapter.ts +1 -0
- package/agent-runtime/index.ts +43 -0
- package/agent-runtime/localLoop.ts +31 -2
- package/agent-runtime/localToolPolicy.ts +1 -0
- package/agent-runtime/localWorkspaceTools.ts +1258 -78
- package/agent-runtime/noloWorkspaceTools.ts +20 -1
- package/agent-runtime/runtimeToolPolicy.ts +1 -5
- package/agent-runtime/runtimeToolSurface.ts +1 -0
- package/agentAliases.ts +29 -9
- package/agentInternalCommandEntries.ts +11 -3
- package/agentMachineCommands.ts +1 -0
- package/agentRecordCommands.ts +2 -2
- package/agentRecordHelpers.ts +59 -6
- package/agentRunCommand.ts +84 -19
- package/ai/agent/buildSystemPrompt.ts +14 -0
- package/ai/agent/cliExecutor.ts +202 -43
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/liveAudioModel.ts +1 -1
- package/ai/agent/streamAgentChatTurn.ts +154 -20
- package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
- package/ai/ai.locale.ts +70 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
- package/ai/llm/kimi.ts +0 -1
- package/ai/llm/openrouterModels.ts +27 -2
- package/ai/memory/policy.ts +49 -0
- package/ai/memory/queryShared.ts +10 -4
- package/ai/memory/recentRelationshipRecap.ts +4 -0
- package/ai/memory/remember.ts +48 -104
- package/ai/memory/scope.ts +74 -0
- package/ai/memory/types.ts +6 -0
- package/ai/memory/understanding.ts +10 -18
- package/ai/policy/runtimePolicy.ts +95 -23
- package/ai/tools/agent/agentTools.ts +0 -21
- package/ai/tools/agent/startAgentDialogTool.ts +2 -1
- package/ai/tools/index.ts +2 -20
- package/ai/tools/prepareTools.ts +171 -1
- package/ai/tools/readXhsProfileTool.ts +80 -69
- package/ai/tools/table/createTableTool.ts +11 -0
- package/ai/tools/toolPacks.ts +19 -0
- package/ai/types.ts +3 -0
- package/app/favorite/deletedFavoriteProjection.ts +31 -0
- package/app/hooks/deleteDbKey.ts +24 -3
- package/app/i18n/translations/interface.locale.ts +48 -16
- package/app/types/appSummary.ts +4 -0
- package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
- package/auth/adminPermissions.ts +64 -4
- package/auth/routes.ts +5 -0
- package/chat/chat.locale.ts +4 -0
- package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
- package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
- package/chat/dialog/dialogSlice.ts +117 -4
- package/chat/messages/activityCompletion.ts +97 -0
- package/chat/messages/imagePayloadPersistence.ts +22 -0
- package/chat/messages/messageContent.ts +20 -16
- package/chat/messages/messageSlice.ts +20 -1
- package/chat/messages/toolThunks.ts +32 -11
- package/chat/messages/types.ts +8 -1
- package/chat/messages/web/toolDisplayName.ts +686 -0
- package/cli/agentAliases.ts +29 -9
- package/cli/agentInternalCommandEntries.ts +11 -3
- package/cli/agentMachineCommands.ts +1 -0
- package/cli/agentRecordCommands.ts +2 -2
- package/cli/agentRecordHelpers.ts +59 -6
- package/cli/agentRunCommand.ts +84 -19
- package/cli/client/agentRun.ts +65 -253
- package/cli/client/localRuntimeAdapter.ts +179 -53
- package/cli/commandRegistry.ts +7 -4
- package/cli/dialogAttachmentCleanup.ts +151 -0
- package/cli/dialogCommands.ts +163 -61
- package/cli/dialogInternalCommandEntries.ts +1 -1
- package/cli/docCreateCommands.ts +345 -0
- package/cli/docInternalCommandEntries.ts +11 -0
- package/cli/docPageHelpers.ts +80 -0
- package/cli/docSkillHelpers.ts +67 -0
- package/cli/docSpaceHelpers.ts +46 -0
- package/cli/internalCommandEntries.ts +4 -0
- package/cli/machineCommands.ts +18 -34
- package/cli/memoryCommands.ts +253 -0
- package/cli/memoryInternalCommandEntries.ts +9 -0
- package/cli/qoderUsageProbe.ts +252 -0
- package/cli/scriptCommandEntries.ts +0 -3
- package/cli/secretScan.ts +31 -0
- package/cli/tableCommands.ts +248 -13
- package/cli/workflowInternalCommandEntries.ts +2 -3
- package/client/agentRun.test.ts +68 -84
- package/client/agentRun.ts +65 -253
- package/client/localRuntimeAdapter.test.ts +505 -74
- package/client/localRuntimeAdapter.ts +179 -53
- package/commandRegistry.ts +7 -4
- package/connector-experimental/capabilities.ts +10 -9
- package/create/space/sidebarVisibleTypes.ts +19 -0
- package/dialogAttachmentCleanup.ts +151 -0
- package/dialogCommands.ts +163 -61
- package/dialogInternalCommandEntries.ts +1 -1
- package/docCreateCommands.ts +345 -0
- package/docInternalCommandEntries.ts +11 -0
- package/docPageHelpers.ts +80 -0
- package/docSkillHelpers.ts +67 -0
- package/docSpaceHelpers.ts +46 -0
- package/integrations/google/models.ts +4 -4
- package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
- package/integrations/xhs-reader/types.ts +36 -3
- package/integrations/xhs-reader/url.ts +24 -1
- package/internalCommandEntries.ts +4 -0
- package/machineCommands.ts +18 -34
- package/memoryCommands.ts +253 -0
- package/memoryInternalCommandEntries.ts +9 -0
- package/package.json +133 -114
- package/qoderUsageProbe.ts +252 -0
- package/render/contentIcon/types.ts +167 -0
- package/render/page/docSlice.ts +30 -3
- package/render/page/types.ts +2 -0
- package/render/table/createTableAction.ts +4 -0
- package/render/table/tableSlice.ts +61 -0
- package/render/table/types.ts +10 -0
- package/scriptCommandEntries.ts +0 -3
- package/secretScan.ts +31 -0
- package/server/handlers/agentRun/types.ts +24 -23
- package/tableCommands.ts +248 -13
- package/workflowInternalCommandEntries.ts +2 -3
- package/ai/tools/agent/taskRunTool.ts +0 -112
- package/cli/client/taskRunPrompt.ts +0 -27
- package/cli/taskRunCommand.ts +0 -265
- package/client/taskRunPrompt.ts +0 -27
- package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
- package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
- package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
- package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
- package/integrations/xhs-reader/normalize.ts +0 -390
- package/integrations/xhs-reader/orchestrator.ts +0 -178
- package/taskRunCommand.ts +0 -265
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export type QoderQuotaUsage = {
|
|
6
|
+
userId: string | null;
|
|
7
|
+
userType: string | null;
|
|
8
|
+
expiresAt: string | null;
|
|
9
|
+
expiresAtMs: number | null;
|
|
10
|
+
isQuotaExceeded: boolean | null;
|
|
11
|
+
totalUsagePercentage: number | null;
|
|
12
|
+
quota: {
|
|
13
|
+
total: number | null;
|
|
14
|
+
used: number | null;
|
|
15
|
+
remaining: number | null;
|
|
16
|
+
percentage: number | null;
|
|
17
|
+
unit: string | null;
|
|
18
|
+
};
|
|
19
|
+
source: "qoder-cli-log";
|
|
20
|
+
logPath?: string;
|
|
21
|
+
capturedAt?: string;
|
|
22
|
+
capturedAtMs?: number | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type QoderUsageFreshness = "fresh" | "stale" | "uncertain";
|
|
26
|
+
|
|
27
|
+
export type QoderUsageProbeResult = {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
provider: "qoder";
|
|
30
|
+
usage?: (QoderQuotaUsage & { logPath: string });
|
|
31
|
+
limitations?: string[];
|
|
32
|
+
freshness?: QoderUsageFreshness;
|
|
33
|
+
ageSeconds?: number | null;
|
|
34
|
+
staleThresholdSeconds?: number;
|
|
35
|
+
scannedLogCount?: number;
|
|
36
|
+
reason?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const QUOTA_RESPONSE_MARKER = "/api/v2/quota/usage response:";
|
|
41
|
+
|
|
42
|
+
const asFiniteNumber = (value: unknown): number | null =>
|
|
43
|
+
typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
44
|
+
|
|
45
|
+
const asString = (value: unknown): string | null =>
|
|
46
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
47
|
+
|
|
48
|
+
const asBoolean = (value: unknown): boolean | null =>
|
|
49
|
+
typeof value === "boolean" ? value : null;
|
|
50
|
+
|
|
51
|
+
function normalizeQuotaPayload(payload: any): QoderQuotaUsage | null {
|
|
52
|
+
if (!payload || typeof payload !== "object") return null;
|
|
53
|
+
const quota = payload.userQuota && typeof payload.userQuota === "object"
|
|
54
|
+
? payload.userQuota
|
|
55
|
+
: {};
|
|
56
|
+
const expiresAtMs = asFiniteNumber(payload.expiresAt);
|
|
57
|
+
return {
|
|
58
|
+
userId: asString(payload.userId),
|
|
59
|
+
userType: asString(payload.userType),
|
|
60
|
+
expiresAt: expiresAtMs != null ? new Date(expiresAtMs).toISOString() : null,
|
|
61
|
+
expiresAtMs,
|
|
62
|
+
isQuotaExceeded: asBoolean(payload.isQuotaExceeded),
|
|
63
|
+
totalUsagePercentage: asFiniteNumber(payload.totalUsagePercentage),
|
|
64
|
+
quota: {
|
|
65
|
+
total: asFiniteNumber(quota.total),
|
|
66
|
+
used: asFiniteNumber(quota.used),
|
|
67
|
+
remaining: asFiniteNumber(quota.remaining),
|
|
68
|
+
percentage: asFiniteNumber(quota.percentage),
|
|
69
|
+
unit: asString(quota.unit),
|
|
70
|
+
},
|
|
71
|
+
source: "qoder-cli-log",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isRealQuotaResponseLine(line: string): boolean {
|
|
76
|
+
const markerIndex = line.indexOf(QUOTA_RESPONSE_MARKER);
|
|
77
|
+
if (markerIndex < 0) return false;
|
|
78
|
+
const before = line.slice(0, markerIndex);
|
|
79
|
+
if (!before.includes("[qoderApi]")) return false;
|
|
80
|
+
const jsonStart = markerIndex + QUOTA_RESPONSE_MARKER.length;
|
|
81
|
+
const after = line.slice(jsonStart).trimStart();
|
|
82
|
+
return after.startsWith("{");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseQoderQuotaUsageFromText(text: string): QoderQuotaUsage | null {
|
|
86
|
+
let latest: QoderQuotaUsage | null = null;
|
|
87
|
+
for (const line of text.split(/\r?\n/)) {
|
|
88
|
+
if (!isRealQuotaResponseLine(line)) continue;
|
|
89
|
+
const markerIndex = line.indexOf(QUOTA_RESPONSE_MARKER);
|
|
90
|
+
const jsonText = line.slice(markerIndex + QUOTA_RESPONSE_MARKER.length).trim();
|
|
91
|
+
try {
|
|
92
|
+
const parsed = normalizeQuotaPayload(JSON.parse(jsonText));
|
|
93
|
+
if (parsed) latest = parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
// Ignore malformed historical log lines.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return latest;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function extractLeadingIsoTimestamp(dirName: string): string | null {
|
|
102
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})(?:-(\d{3}))?([+-])(\d{2})-(\d{2})/.exec(dirName);
|
|
103
|
+
if (!match) return null;
|
|
104
|
+
const [, Y, M, D, h, m, s, ms, sign, offH, offM] = match;
|
|
105
|
+
const local = Date.UTC(Number(Y), Number(M) - 1, Number(D), Number(h), Number(m), Number(s), Number(ms ?? 0));
|
|
106
|
+
const offsetMs = (Number(offH) * 60 + Number(offM)) * 60_000;
|
|
107
|
+
const utcMs = sign === "+" ? local - offsetMs : local + offsetMs;
|
|
108
|
+
if (!Number.isFinite(utcMs)) return null;
|
|
109
|
+
return new Date(utcMs).toISOString();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type RunLogEntry = {
|
|
113
|
+
logPath: string;
|
|
114
|
+
capturedAtMs: number | null;
|
|
115
|
+
capturedAt: string | null;
|
|
116
|
+
dirName: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function listQoderRunLogs(qoderHome: string): RunLogEntry[] {
|
|
120
|
+
const runsDir = join(qoderHome, "logs", "runs");
|
|
121
|
+
if (!existsSync(runsDir)) return [];
|
|
122
|
+
const entries: RunLogEntry[] = [];
|
|
123
|
+
for (const entry of readdirSync(runsDir, { withFileTypes: true })) {
|
|
124
|
+
if (!entry.isDirectory()) continue;
|
|
125
|
+
const logPath = join(runsDir, entry.name, "qodercli.log");
|
|
126
|
+
if (!existsSync(logPath)) continue;
|
|
127
|
+
const capturedAt = extractLeadingIsoTimestamp(entry.name);
|
|
128
|
+
const capturedAtMs = capturedAt ? Date.parse(capturedAt) : null;
|
|
129
|
+
entries.push({
|
|
130
|
+
logPath,
|
|
131
|
+
capturedAtMs,
|
|
132
|
+
capturedAt,
|
|
133
|
+
dirName: entry.name,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
entries.sort((a, b) => {
|
|
137
|
+
const aMs = a.capturedAtMs ?? statSync(a.logPath).mtimeMs;
|
|
138
|
+
const bMs = b.capturedAtMs ?? statSync(b.logPath).mtimeMs;
|
|
139
|
+
return bMs - aMs;
|
|
140
|
+
});
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function findLatestQoderQuotaUsage(options: {
|
|
145
|
+
qoderHome?: string;
|
|
146
|
+
now?: number;
|
|
147
|
+
} = {}): (QoderQuotaUsage & { logPath: string; capturedAtMs: number | null; capturedAt: string | null }) | null {
|
|
148
|
+
const qoderHome = options.qoderHome || join(homedir(), ".qoder");
|
|
149
|
+
for (const run of listQoderRunLogs(qoderHome)) {
|
|
150
|
+
const parsed = parseQoderQuotaUsageFromText(readFileSync(run.logPath, "utf8"));
|
|
151
|
+
if (!parsed) continue;
|
|
152
|
+
return {
|
|
153
|
+
...parsed,
|
|
154
|
+
logPath: run.logPath,
|
|
155
|
+
capturedAt: run.capturedAt,
|
|
156
|
+
capturedAtMs: run.capturedAtMs,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const DEFAULT_STALE_THRESHOLD_SECONDS = 6 * 60 * 60;
|
|
163
|
+
|
|
164
|
+
export function classifyQoderUsageFreshness(
|
|
165
|
+
usage: { capturedAtMs: number | null } | null,
|
|
166
|
+
options: { now?: number; staleThresholdSeconds?: number } = {},
|
|
167
|
+
): QoderUsageFreshness {
|
|
168
|
+
if (!usage || usage.capturedAtMs == null) return "uncertain";
|
|
169
|
+
const now = options.now ?? Date.now();
|
|
170
|
+
const threshold = (options.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS) * 1000;
|
|
171
|
+
const age = now - usage.capturedAtMs;
|
|
172
|
+
if (!Number.isFinite(age) || age < 0) return "uncertain";
|
|
173
|
+
return age > threshold ? "stale" : "fresh";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readOption(args: string[], flag: string) {
|
|
177
|
+
const index = args.indexOf(flag);
|
|
178
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function runQoderUsageCommand(
|
|
182
|
+
args: string[],
|
|
183
|
+
deps: {
|
|
184
|
+
env?: Record<string, string | undefined>;
|
|
185
|
+
output?: { write(chunk: string): unknown };
|
|
186
|
+
now?: () => number;
|
|
187
|
+
} = {},
|
|
188
|
+
) {
|
|
189
|
+
const output = deps.output ?? process.stdout;
|
|
190
|
+
const agent = args[0]?.trim();
|
|
191
|
+
if (!agent || agent === "--help" || agent === "-h") {
|
|
192
|
+
output.write("Usage: nolo agent usage qoder [--qoder-home <path>] [--stale-threshold-seconds <n>]\n");
|
|
193
|
+
return agent ? 0 : 1;
|
|
194
|
+
}
|
|
195
|
+
if (agent.toLowerCase() !== "qoder") {
|
|
196
|
+
output.write(`[nolo] agent usage currently supports qoder only, received ${agent}.\n`);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
const qoderHome =
|
|
200
|
+
readOption(args, "--qoder-home") ||
|
|
201
|
+
deps.env?.QODER_HOME ||
|
|
202
|
+
join(homedir(), ".qoder");
|
|
203
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
204
|
+
const thresholdArg = readOption(args, "--stale-threshold-seconds");
|
|
205
|
+
const staleThresholdSeconds = thresholdArg ? Number(thresholdArg) : DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
206
|
+
const usage = findLatestQoderQuotaUsage({ qoderHome, now });
|
|
207
|
+
const freshness = classifyQoderUsageFreshness(usage, { now, staleThresholdSeconds });
|
|
208
|
+
const ageSeconds = usage?.capturedAtMs != null ? Math.max(0, Math.floor((now - usage.capturedAtMs) / 1000)) : null;
|
|
209
|
+
|
|
210
|
+
if (!usage) {
|
|
211
|
+
const result: QoderUsageProbeResult = {
|
|
212
|
+
ok: false,
|
|
213
|
+
provider: "qoder",
|
|
214
|
+
freshness,
|
|
215
|
+
ageSeconds,
|
|
216
|
+
staleThresholdSeconds,
|
|
217
|
+
limitations: [
|
|
218
|
+
"This probe only reads Qoder package Credits from local quota logs.",
|
|
219
|
+
"It does not currently read the Qwen3.7-Max daily free request quota; check Qoder's Usage Overview or CLI /usage for that counter.",
|
|
220
|
+
],
|
|
221
|
+
error:
|
|
222
|
+
"No Qoder quota usage log found. Open `qoder`, run `/usage`, then retry this command.",
|
|
223
|
+
reason:
|
|
224
|
+
"Probe could not locate any real [qoderApi] quota response line in local run logs.",
|
|
225
|
+
};
|
|
226
|
+
output.write(JSON.stringify(result, null, 2));
|
|
227
|
+
output.write("\n");
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result: QoderUsageProbeResult = {
|
|
232
|
+
ok: true,
|
|
233
|
+
provider: "qoder",
|
|
234
|
+
usage,
|
|
235
|
+
limitations: [
|
|
236
|
+
"This is package Credits usage only, parsed from Qoder's /api/v2/quota/usage log.",
|
|
237
|
+
"Qwen3.7-Max daily free requests are a separate counter and are not exposed in this probe's current log source.",
|
|
238
|
+
],
|
|
239
|
+
freshness,
|
|
240
|
+
ageSeconds,
|
|
241
|
+
staleThresholdSeconds,
|
|
242
|
+
};
|
|
243
|
+
if (freshness !== "fresh") {
|
|
244
|
+
result.reason =
|
|
245
|
+
freshness === "stale"
|
|
246
|
+
? `Latest local quota log is ${ageSeconds}s old (threshold ${staleThresholdSeconds}s); UI or server may show a different value.`
|
|
247
|
+
: "Latest local quota log has no reliable capture timestamp; treat numbers as uncertain.";
|
|
248
|
+
}
|
|
249
|
+
output.write(JSON.stringify(result, null, 2));
|
|
250
|
+
output.write("\n");
|
|
251
|
+
return freshness === "fresh" ? 0 : 2;
|
|
252
|
+
}
|
|
@@ -2,15 +2,12 @@ import { createScriptCommand, type CommandEntry } from "./cliCommandTypes";
|
|
|
2
2
|
|
|
3
3
|
function getDocScriptCommands(): CommandEntry[] {
|
|
4
4
|
return [
|
|
5
|
-
createScriptCommand(["doc", "create"], "createDoc.ts", "Create a normal doc"),
|
|
6
5
|
createScriptCommand(["doc", "read"], "readDoc.ts", "Read a normal doc"),
|
|
7
6
|
createScriptCommand(["doc", "update"], "updateDoc.ts", "Update a normal doc"),
|
|
8
7
|
createScriptCommand(["doc", "delete"], "deleteDoc.ts", "Delete a normal doc"),
|
|
9
|
-
createScriptCommand(["skill-doc", "create"], "createSkillDoc.ts", "Create a skill doc"),
|
|
10
8
|
createScriptCommand(["skill-doc", "read"], "readSkillDoc.ts", "Read a skill doc"),
|
|
11
9
|
createScriptCommand(["skill-doc", "update"], "updateSkillDoc.ts", "Update a skill doc"),
|
|
12
10
|
createScriptCommand(["skill-doc", "delete"], "deleteSkillDoc.ts", "Delete a skill doc"),
|
|
13
|
-
createScriptCommand(["skill", "create-doc"], "createSkillDoc.ts", "Create a skill-backed doc"),
|
|
14
11
|
];
|
|
15
12
|
}
|
|
16
13
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type SecretFinding = {
|
|
2
|
+
label: string;
|
|
3
|
+
line: number;
|
|
4
|
+
preview: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const SECRET_PATTERNS: Array<{ label: string; pattern: RegExp }> = [
|
|
8
|
+
{ label: "password", pattern: /\bpassword\s*[:=]\s*["']?[^"',\s#]{8,}/i },
|
|
9
|
+
{ label: "psk", pattern: /\bpsk\s*[:=]\s*["']?[^"',\s#]{8,}/i },
|
|
10
|
+
{ label: "api key", pattern: /\b(api[_-]?key|apikey)\s*[:=]\s*["']?[^"',\s#]{12,}/i },
|
|
11
|
+
{ label: "token", pattern: /\b(token|secret)\s*[:=]\s*["']?[^"',\s#]{12,}/i },
|
|
12
|
+
{ label: "private key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function findPotentialSecrets(text: string): SecretFinding[] {
|
|
16
|
+
return text
|
|
17
|
+
.split(/\r?\n/)
|
|
18
|
+
.flatMap((line, index) =>
|
|
19
|
+
SECRET_PATTERNS.filter(({ pattern }) => pattern.test(line)).map(({ label }) => ({
|
|
20
|
+
label,
|
|
21
|
+
line: index + 1,
|
|
22
|
+
preview: line.length > 120 ? `${line.slice(0, 117)}...` : line,
|
|
23
|
+
}))
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatSecretFindings(findings: SecretFinding[]) {
|
|
28
|
+
return findings
|
|
29
|
+
.map((finding) => ` line ${finding.line} (${finding.label}): ${finding.preview}`)
|
|
30
|
+
.join("\n");
|
|
31
|
+
}
|
package/cli/tableCommands.ts
CHANGED
|
@@ -55,6 +55,11 @@ function parseOutputMode(raw: string): OutputMode {
|
|
|
55
55
|
throw new Error(`--output must be one of full, raw, items, json, jsonl; got ${raw}`);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function resolveQueryColumns(columns: string[] | undefined, includeActivity: boolean): string[] | undefined {
|
|
59
|
+
if (!includeActivity) return columns;
|
|
60
|
+
return Array.isArray(columns) ? includeTableActivityColumns(columns) : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
function parseTableArg(raw: string): { tenantId?: string; tableId?: string } {
|
|
59
64
|
const trimmed = raw.trim();
|
|
60
65
|
if (!trimmed) return {};
|
|
@@ -66,6 +71,32 @@ function parseTableArg(raw: string): { tenantId?: string; tableId?: string } {
|
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
function normalizeSpaceInput(raw: string): string {
|
|
75
|
+
const value = raw.trim();
|
|
76
|
+
if (!value) return "";
|
|
77
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
78
|
+
const match = new URL(value).pathname.match(/^\/space\/([^/]+)/);
|
|
79
|
+
return match?.[1] ? decodeURIComponent(match[1]).replace(/^space-/, "") : "";
|
|
80
|
+
}
|
|
81
|
+
return value.replace(/^space-/, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSpaceContentKeys(spaceRecord: any): Set<string> {
|
|
85
|
+
const keys = new Set<string>();
|
|
86
|
+
const contents = spaceRecord?.contents;
|
|
87
|
+
if (!contents || typeof contents !== "object") return keys;
|
|
88
|
+
for (const [entryKey, value] of Object.entries(contents)) {
|
|
89
|
+
keys.add(entryKey);
|
|
90
|
+
if (value && typeof value === "object") {
|
|
91
|
+
const contentKey = (value as any).contentKey;
|
|
92
|
+
if (typeof contentKey === "string" && contentKey.trim()) {
|
|
93
|
+
keys.add(contentKey.trim());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return keys;
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
function resolveServerUrl(args: string[], env: EnvLike): string {
|
|
70
101
|
return (
|
|
71
102
|
readOption(args, "--server-url") ||
|
|
@@ -89,16 +120,46 @@ function resolveAuthToken(args: string[], env: EnvLike): string {
|
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
function parseJwtTenantId(token: string): string | undefined {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const parsed = JSON.parse(decoded);
|
|
98
|
-
return typeof parsed.userId === "string" ? parsed.userId : typeof parsed.sub === "string" ? parsed.sub : undefined;
|
|
99
|
-
} catch {
|
|
100
|
-
return undefined;
|
|
123
|
+
for (const parsed of parseJwtPayloadCandidates(token)) {
|
|
124
|
+
const userId = parsed.userId;
|
|
125
|
+
const sub = parsed.sub;
|
|
126
|
+
if (typeof userId === "string" && userId) return userId;
|
|
127
|
+
if (typeof sub === "string" && sub) return sub;
|
|
101
128
|
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseJwtUsername(token: string): string | undefined {
|
|
133
|
+
for (const parsed of parseJwtPayloadCandidates(token)) {
|
|
134
|
+
const username = parsed.username;
|
|
135
|
+
if (typeof username === "string" && username.trim()) return username.trim();
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseJwtPayloadCandidates(token: string): Array<Record<string, unknown>> {
|
|
141
|
+
const trimmed = token.trim();
|
|
142
|
+
if (!trimmed) return [];
|
|
143
|
+
const parts = trimmed.split(".").filter(Boolean);
|
|
144
|
+
const payloadCandidates = parts.length >= 3 ? [parts[1], parts[0]] : [parts[0]];
|
|
145
|
+
const parsed: Array<Record<string, unknown>> = [];
|
|
146
|
+
for (const payload of payloadCandidates) {
|
|
147
|
+
try {
|
|
148
|
+
const decoded = Buffer.from(payload, "base64").toString("utf8");
|
|
149
|
+
const value = JSON.parse(decoded);
|
|
150
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
151
|
+
parsed.push(value as Record<string, unknown>);
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveUserId(args: string[], env: EnvLike, authToken: string): string | undefined {
|
|
159
|
+
const explicit = readOption(args, "--tenant-id") || env.USER_ID || parseJwtTenantId(authToken);
|
|
160
|
+
if (explicit) return explicit;
|
|
161
|
+
const username = parseJwtUsername(authToken);
|
|
162
|
+
return username && /^[a-z0-9_-]+$/i.test(username) ? username : undefined;
|
|
102
163
|
}
|
|
103
164
|
|
|
104
165
|
function formatOutput(input: { envelope: any; rawData: any; mode: OutputMode }): string {
|
|
@@ -133,6 +194,182 @@ function usage(): string {
|
|
|
133
194
|
].join("\n");
|
|
134
195
|
}
|
|
135
196
|
|
|
197
|
+
function listUsage(): string {
|
|
198
|
+
return [
|
|
199
|
+
"Usage:",
|
|
200
|
+
" nolo table list [--limit <n>] [--title-query <text>] [--purpose <purpose>] [--output full|raw|items|json|jsonl]",
|
|
201
|
+
"",
|
|
202
|
+
].join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function tableHasPurpose(table: any, purpose: string): boolean {
|
|
206
|
+
return typeof table?.purpose === "string" && table.purpose.toLowerCase() === purpose.toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getComparableUpdatedAt(record: any): number {
|
|
210
|
+
const raw = record?.updatedAt ?? record?.updated_at ?? record?.createdAt ?? record?.created;
|
|
211
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
212
|
+
if (typeof raw === "string") return Date.parse(raw) || 0;
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatTableListOutput(args: {
|
|
217
|
+
mode: OutputMode;
|
|
218
|
+
tables: any[];
|
|
219
|
+
envelope: any;
|
|
220
|
+
}) {
|
|
221
|
+
if (args.mode === "full") return JSON.stringify(args.envelope, null, 2);
|
|
222
|
+
if (args.mode === "raw") return JSON.stringify(args.tables, null, 2);
|
|
223
|
+
if (args.mode === "items") return JSON.stringify(args.tables, null, 2);
|
|
224
|
+
return args.tables.map((item) => JSON.stringify(item)).join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function runTableListCommand(args: string[], deps: TableCommandDeps = {}): Promise<number> {
|
|
228
|
+
const env = deps.env ?? process.env;
|
|
229
|
+
const output = deps.output ?? process.stdout;
|
|
230
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
231
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
232
|
+
output.write(listUsage());
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let outputMode: OutputMode;
|
|
237
|
+
try {
|
|
238
|
+
outputMode = parseOutputMode(readOption(args, "--output") || "items");
|
|
239
|
+
} catch (error) {
|
|
240
|
+
output.write(`[nolo] table list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const authToken = resolveAuthToken(args, env);
|
|
245
|
+
const userId = resolveUserId(args, env, authToken);
|
|
246
|
+
if (!userId) {
|
|
247
|
+
output.write("[nolo] table list failed: user id is required. Pass --tenant-id or use a login token.\n");
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
if (!authToken) {
|
|
251
|
+
output.write("[nolo] table list failed: AUTH_TOKEN is required.\n");
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const limit = Math.max(1, Math.min(Number(readOption(args, "--limit") || 50), 200));
|
|
256
|
+
const titleQuery = readOption(args, "--title-query").trim().toLowerCase();
|
|
257
|
+
const purpose = readOption(args, "--purpose").trim();
|
|
258
|
+
const spaceId = normalizeSpaceInput(readOption(args, "--space") || readOption(args, "--space-id"));
|
|
259
|
+
const scanLimit = titleQuery || purpose ? Math.max(limit * 5, 200) : limit;
|
|
260
|
+
const serverUrl = resolveServerUrl(args, env);
|
|
261
|
+
|
|
262
|
+
let records: any[];
|
|
263
|
+
try {
|
|
264
|
+
records = spaceId
|
|
265
|
+
? await fetchSpaceTableRecords({
|
|
266
|
+
authToken,
|
|
267
|
+
fetchImpl,
|
|
268
|
+
serverUrl,
|
|
269
|
+
spaceId,
|
|
270
|
+
})
|
|
271
|
+
: await fetchUserTableRecords({
|
|
272
|
+
authToken,
|
|
273
|
+
fetchImpl,
|
|
274
|
+
limit: Math.min(scanLimit, 500),
|
|
275
|
+
serverUrl,
|
|
276
|
+
userId,
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
output.write(`[nolo] table list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
const tables = records
|
|
283
|
+
.filter((table: any) => table && typeof table === "object" && !table.deletedAt)
|
|
284
|
+
.filter((table: any) => !titleQuery || String(table.displayName ?? table.name ?? table.title ?? "").toLowerCase().includes(titleQuery))
|
|
285
|
+
.filter((table: any) => !purpose || tableHasPurpose(table, purpose))
|
|
286
|
+
.sort((left: any, right: any) => getComparableUpdatedAt(right) - getComparableUpdatedAt(left))
|
|
287
|
+
.slice(0, limit)
|
|
288
|
+
.map((table: any) => ({
|
|
289
|
+
dbKey: table.dbKey ?? null,
|
|
290
|
+
tableId: table.tableId ?? null,
|
|
291
|
+
displayName: table.displayName ?? table.name ?? table.title ?? "(untitled)",
|
|
292
|
+
spaceId: table.spaceId ?? null,
|
|
293
|
+
tenantId: table.tenantId ?? null,
|
|
294
|
+
columnCount: Array.isArray(table.columns) ? table.columns.length : 0,
|
|
295
|
+
createdAt: table.createdAt ?? null,
|
|
296
|
+
updatedAt: table.updatedAt ?? table.updated_at ?? null,
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
const envelope = { success: true, userId, ...(spaceId ? { space: spaceId } : {}), total: tables.length, tables };
|
|
300
|
+
output.write(`${formatTableListOutput({ mode: outputMode, tables, envelope })}\n`);
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function readJsonResponse(response: Response) {
|
|
305
|
+
const text = await response.text();
|
|
306
|
+
let payload: any;
|
|
307
|
+
try {
|
|
308
|
+
payload = text ? JSON.parse(text) : {};
|
|
309
|
+
} catch {
|
|
310
|
+
if (!response.ok) throw new Error(text || response.statusText);
|
|
311
|
+
return text;
|
|
312
|
+
}
|
|
313
|
+
if (!response.ok || payload?.error) {
|
|
314
|
+
throw new Error(payload?.error ?? response.statusText);
|
|
315
|
+
}
|
|
316
|
+
return payload;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function fetchUserTableRecords(args: {
|
|
320
|
+
authToken: string;
|
|
321
|
+
fetchImpl: typeof fetch;
|
|
322
|
+
limit: number;
|
|
323
|
+
serverUrl: string;
|
|
324
|
+
userId: string;
|
|
325
|
+
}) {
|
|
326
|
+
const response = await args.fetchImpl(
|
|
327
|
+
`${args.serverUrl}/api/v1/db/query/${encodeURIComponent(args.userId)}?limit=${args.limit}`,
|
|
328
|
+
{
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: {
|
|
331
|
+
Authorization: `Bearer ${args.authToken}`,
|
|
332
|
+
"Content-Type": "application/json",
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify({ type: "table" }),
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
const payload = await readJsonResponse(response);
|
|
338
|
+
return Array.isArray(payload?.data?.data)
|
|
339
|
+
? payload.data.data
|
|
340
|
+
: Array.isArray(payload?.data)
|
|
341
|
+
? payload.data
|
|
342
|
+
: [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function fetchSpaceTableRecords(args: {
|
|
346
|
+
authToken: string;
|
|
347
|
+
fetchImpl: typeof fetch;
|
|
348
|
+
serverUrl: string;
|
|
349
|
+
spaceId: string;
|
|
350
|
+
}) {
|
|
351
|
+
const readRecord = async (dbKey: string) => {
|
|
352
|
+
const response = await args.fetchImpl(
|
|
353
|
+
`${args.serverUrl}/api/v1/db/read/${encodeURIComponent(dbKey)}`,
|
|
354
|
+
{ headers: { Authorization: `Bearer ${args.authToken}` } }
|
|
355
|
+
);
|
|
356
|
+
const payload = await readJsonResponse(response);
|
|
357
|
+
return payload?.data ?? payload;
|
|
358
|
+
};
|
|
359
|
+
const space = await readRecord(`space-${args.spaceId}`);
|
|
360
|
+
const tableKeys = [...getSpaceContentKeys(space)].filter((key) => key.startsWith("meta-"));
|
|
361
|
+
const records = await Promise.all(
|
|
362
|
+
tableKeys.map(async (key) => {
|
|
363
|
+
try {
|
|
364
|
+
return await readRecord(key);
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
return records.filter(Boolean);
|
|
371
|
+
}
|
|
372
|
+
|
|
136
373
|
export async function runTableQueryCommand(args: string[], deps: TableCommandDeps = {}): Promise<number> {
|
|
137
374
|
const env = deps.env ?? process.env;
|
|
138
375
|
const output = deps.output ?? process.stdout;
|
|
@@ -159,7 +396,7 @@ export async function runTableQueryCommand(args: string[], deps: TableCommandDep
|
|
|
159
396
|
|
|
160
397
|
const tableArg = parseTableArg(readOption(args, "--table"));
|
|
161
398
|
const authToken = resolveAuthToken(args, env);
|
|
162
|
-
const tenantId = readOption(args, "--tenant-id") || tableArg.tenantId || env
|
|
399
|
+
const tenantId = readOption(args, "--tenant-id") || tableArg.tenantId || resolveUserId(args, env, authToken);
|
|
163
400
|
const tableId = tableArg.tableId;
|
|
164
401
|
const hasSingleRowShortcut = Boolean(readOption(args, "--row") || readOption(args, "--row-id") || readOption(args, "--row-dbkey"));
|
|
165
402
|
if (!tenantId || !tableId) {
|
|
@@ -184,9 +421,7 @@ export async function runTableQueryCommand(args: string[], deps: TableCommandDep
|
|
|
184
421
|
tenantId,
|
|
185
422
|
tableId,
|
|
186
423
|
filters: filters ?? {},
|
|
187
|
-
columns: hasFlag(args, "--include-activity")
|
|
188
|
-
? includeTableActivityColumns(columns)
|
|
189
|
-
: columns,
|
|
424
|
+
columns: resolveQueryColumns(columns, hasFlag(args, "--include-activity")),
|
|
190
425
|
includeBaseFields: !hasFlag(args, "--no-base-fields"),
|
|
191
426
|
limit: Number(readOption(args, "--limit") || (hasSingleRowShortcut ? 1 : 20)),
|
|
192
427
|
offset: Number(readOption(args, "--offset") || 0),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createEnvCommand } from "./cliCommandFactories";
|
|
2
2
|
import type { CommandEntry } from "./cliCommandTypes";
|
|
3
|
-
import { runTableQueryCommand } from "./tableCommands";
|
|
4
|
-
import { runTaskRunCommand } from "./taskRunCommand";
|
|
3
|
+
import { runTableListCommand, runTableQueryCommand } from "./tableCommands";
|
|
5
4
|
|
|
6
5
|
export function getWorkflowInternalCommandEntries(): CommandEntry[] {
|
|
7
6
|
return [
|
|
7
|
+
createEnvCommand(["table", "list"], "List table metadata", runTableListCommand),
|
|
8
8
|
createEnvCommand(["table", "query"], "Query table rows", runTableQueryCommand),
|
|
9
|
-
createEnvCommand(["task-run"], "Update durable task-run state through the runner bridge", runTaskRunCommand),
|
|
10
9
|
];
|
|
11
10
|
}
|