wind-agent-cli 1.38.3 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/api-docs.html +4 -3
- package/dist/assets/{CodeEditor-C9qFO6L8.js → CodeEditor-C8eMIGen.js} +1 -1
- package/dist/assets/{FileTree-0yJEzBi9.js → FileTree-D52nAPO4.js} +1 -1
- package/dist/assets/{GitPanel-BhbG7yVZ.js → GitPanel-DgeKbZsW.js} +1 -1
- package/dist/assets/{MarkdownPreview-C7pAt3xI.js → MarkdownPreview-B00VQTx6.js} +1 -1
- package/dist/assets/{Shell-CrjXAW4f.js → Shell-twzi9t5B.js} +7 -7
- package/dist/assets/{StandaloneShell-J5BR1g26.js → StandaloneShell-DH_GBJMj.js} +1 -1
- package/dist/assets/{TaskMasterPanel-B2wpEHWP.js → TaskMasterPanel-BY-pMPMl.js} +2 -2
- package/dist/assets/{abnfDiagram-N423BO3Z-DztbjsU0.js → abnfDiagram-N423BO3Z-BwBdstkH.js} +1 -1
- package/dist/assets/{arc-DRGumrCP.js → arc-C0IOpbFP.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-DnsczqwM.js → architectureDiagram-T3A2C74G-C-Wkj_AY.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-CPsS5qw5.js → blockDiagram-VBNYF7ZC-_sXAZKku.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-DyjwDxYL.js → c4Diagram-5PPSVZJV-C0DUsZv8.js} +1 -1
- package/dist/assets/channel-B6xiB58V.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-C794q4RK.js → chunk-2GRJ4B5K-D2TFB_6A.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-BfK76GMF.js → chunk-2Q5K7J3B-B_co_2bI.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-Dmwk_KHT.js → chunk-5RXB4S5H-BcUSLq4H.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DOeUnsxL.js → chunk-5VM5RSS4-0mQFBKSp.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-BrjdaybI.js → chunk-6Q2QTUOP-BAkqmW1M.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-DCjoLr5D.js → chunk-GF5L2VYU-Cyz0Xl7f.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-TEJEfxQK.js → chunk-JWPE2WC7-DYO47aT-.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-tbXW-dpx.js → chunk-KBJHAD2P-DvXEzV8J.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CX4Zsite.js → chunk-RYQCIY6F-Beut8dYt.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DVJXNcFN.js → chunk-XXDRQBXY-DLIy1GFl.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-C7xIPTll.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-C7xIPTll.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-DLpXc-vJ.js → cose-bilkent-JH36ORCC-DynbO4dj.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-toFyL_eT.js → cynefin-VYW2F7L2-CYTJsH3o.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-7KnX5nu-.js → cynefinDiagram-MW4NZA55-ftj-lAeE.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-C-QOFsDs.js → dagre-VZM6K2ZE-C1kSFboe.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-26OmKaGK.js → diagram-7IWD3JNH-CqXHTFrY.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-0o9kK2uO.js → diagram-B4RE2ZJO-B84n3Zko.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-CAgMu3zd.js → diagram-LBJQPF4R-B710SefS.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-B0W-AePB.js → diagram-Q27KOJAE-CmVPl9pq.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-66AXZ7Mk.js → diagram-UB23O5K3-LozbeaFL.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-JlixVrAW.js → ebnfDiagram-BXEA7PRR-B321UBUA.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-ZxzuRr4f.js → erDiagram-JOGREHBK-CTbw676k.js} +1 -1
- package/dist/assets/feature-chat-pY0kOrz2.js +88 -0
- package/dist/assets/feature-workspace-FLomYkPq.js +39 -0
- package/dist/assets/{flowDiagram-UKHOOZJN-u67vU32D.js → flowDiagram-UKHOOZJN-BWVKTWlw.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-Crl9IPqX.js → ganttDiagram-PKOTCBZU-BAj9eeeX.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-B2rTRd1W.js → gitGraphDiagram-DS77QQ5N-DcVMRU7N.js} +1 -1
- package/dist/assets/{index-YP8FmBZO.js → index-BYFstHFT.js} +1 -1
- package/dist/assets/index-BbBSplOU.js +35 -0
- package/dist/assets/{index-CPP7a6gU.js → index-BrWTp075.js} +1 -1
- package/dist/assets/index-CHukVVB2.css +1 -0
- package/dist/assets/{index-BTFkWJZe.js → index-k9M0VaFg.js} +1 -1
- package/dist/assets/{infoDiagram-6WML65LV-o9ZSr_ud.js → infoDiagram-6WML65LV-V6wz33B4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DnrbX4Ui.js → ishikawaDiagram-WSZJBQD7-Dd-WNPyK.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-CSfWpt5R.js → journeyDiagram-NVQOT4AX-D9VCuCeA.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-dj3PZxCQ.js → kanban-definition-27J2QSJJ-CUtwDOl-.js} +1 -1
- package/dist/assets/{linear-DieERJn8.js → linear-D_XGf1jO.js} +1 -1
- package/dist/assets/{mermaid.core-cq_VDJvZ.js → mermaid.core-DOnsXioB.js} +4 -4
- package/dist/assets/{mindmap-definition-FAOFIHXS-CHQ8W4LE.js → mindmap-definition-FAOFIHXS-DAYNL5ZV.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-CR2rMB3e.js → pegDiagram-VL7TDLO6-B5FX3-E6.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-Cs4NOlmu.js → pieDiagram-7S7Q4E2Y-EbnDBVd0.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-CdHqIaj5.js → quadrantDiagram-CIZ2JOQS-C1koOtwT.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-BNPU0Q8Z.js → railroadDiagram-AXF67PYL-D52xmshj.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-DwDQoirA.js → requirementDiagram-LRYGKXZP-B7VbfNnW.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-DSDSpLjf.js → sankeyDiagram-W5VNT64P-CeRG1vHN.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-zfhext7x.js → sequenceDiagram-SI44F4Z6-CfKHnmSE.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-C56q6k2T.js → sizeCapture-X5ZJPWSS-DJTzyd0S.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-D2lhrHPg.js → stateDiagram-OKZ733FA-DN0c7IYD.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-DbihlVOQ.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-D1HyP0oO.js → swimlanes-SLNWSIFB-CaaJpBDA.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-BBRevuGP.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-R1zaf6JI.js → timeline-definition-Z64GVDOM-CrzHN7d2.js} +1 -1
- package/dist/assets/{vennDiagram-T6HMQDX7-BwlaSNYk.js → vennDiagram-T6HMQDX7-BTldHIjo.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-Dd9-WAib.js → wardleyDiagram-T6FBY63Y-D9Y50OsZ.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-BVn4eBvu.js → xychartDiagram-ELKLHX3M-DjxSJ3aF.js} +1 -1
- package/dist/favicon.png +0 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-180x180.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/icon-maskable-192x192.png +0 -0
- package/dist/icons/icon-maskable-512x512.png +0 -0
- package/dist/icons/new.png +0 -0
- package/dist/index.html +4 -4
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/sw.js +1 -1
- package/dist-server/server/index.js +2 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +14 -2
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +1 -0
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/commands/commands.routes.js +2 -1
- package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +43 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/tests/projects.db.integration.test.js +24 -1
- package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +2 -0
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +10 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/pi/index.js +5 -0
- package/dist-server/server/modules/providers/list/pi/index.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-auth.provider.js +55 -0
- package/dist-server/server/modules/providers/list/pi/pi-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-mcp.provider.js +23 -0
- package/dist-server/server/modules/providers/list/pi/pi-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-models.provider.js +99 -0
- package/dist-server/server/modules/providers/list/pi/pi-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-runtime.provider.js +262 -0
- package/dist-server/server/modules/providers/list/pi/pi-runtime.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-session-synchronizer.provider.js +136 -0
- package/dist-server/server/modules/providers/list/pi/pi-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-sessions.provider.js +225 -0
- package/dist-server/server/modules/providers/list/pi/pi-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi-skills.provider.js +31 -0
- package/dist-server/server/modules/providers/list/pi/pi-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/pi/pi.provider.js +22 -0
- package/dist-server/server/modules/providers/list/pi/pi.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +2 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +26 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +5 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/mcp.test.js +4 -2
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +56 -5
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/pi-provider.test.js +244 -0
- package/dist-server/server/modules/providers/tests/pi-provider.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +9 -0
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
- package/dist-server/server/shared/utils.js +19 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +1 -1
- package/public/api-docs.html +4 -3
- package/scripts/generate-pwa-icons.ps1 +1 -1
- package/server/index.ts +2 -0
- package/server/modules/agent/agent.module.ts +1 -1
- package/server/modules/agent/agent.routes.ts +15 -2
- package/server/modules/agent/tests/agent.routes.test.ts +1 -0
- package/server/modules/commands/commands.routes.ts +2 -1
- package/server/modules/database/migrations.ts +52 -0
- package/server/modules/database/tests/projects.db.integration.test.ts +28 -1
- package/server/modules/notifications/services/notification-orchestrator.service.js +2 -0
- package/server/modules/providers/README.md +5 -1
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -0
- package/server/modules/providers/list/pi/index.ts +4 -0
- package/server/modules/providers/list/pi/pi-auth.provider.ts +58 -0
- package/server/modules/providers/list/pi/pi-mcp.provider.ts +33 -0
- package/server/modules/providers/list/pi/pi-models.provider.ts +114 -0
- package/server/modules/providers/list/pi/pi-runtime.provider.ts +271 -0
- package/server/modules/providers/list/pi/pi-session-synchronizer.provider.ts +134 -0
- package/server/modules/providers/list/pi/pi-sessions.provider.ts +206 -0
- package/server/modules/providers/list/pi/pi-skills.provider.ts +37 -0
- package/server/modules/providers/list/pi/pi.provider.ts +33 -0
- package/server/modules/providers/provider.registry.ts +2 -0
- package/server/modules/providers/provider.routes.ts +1 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +32 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +5 -0
- package/server/modules/providers/tests/mcp.test.ts +4 -2
- package/server/modules/providers/tests/opencode-sessions.test.ts +81 -6
- package/server/modules/providers/tests/pi-provider.test.ts +244 -0
- package/server/modules/providers/tests/provider-runtime.service.test.ts +10 -0
- package/server/shared/types.ts +1 -1
- package/server/shared/utils.ts +23 -0
- package/dist/assets/channel-Dj-iU3BT.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-euwGMP6E.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-euwGMP6E.js +0 -1
- package/dist/assets/feature-chat-CNYDDkFQ.js +0 -88
- package/dist/assets/feature-workspace-BMlv7Ust.js +0 -39
- package/dist/assets/index-Cr5MbHuj.css +0 -1
- package/dist/assets/index-uINlYqXe.js +0 -35
- package/dist/assets/stateDiagram-v2-UEYNNEHI-C3wzBtFn.js +0 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-Dts8yQMu.js +0 -8
- package/dist/icons/new-icon.png +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
|
+
|
|
7
|
+
import type { IProviderModels } from '@/shared/interfaces.js';
|
|
8
|
+
import type { ProviderCurrentActiveModel, ProviderModelOption, ProviderModelsDefinition } from '@/shared/types.js';
|
|
9
|
+
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
|
|
10
|
+
|
|
11
|
+
const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
12
|
+
|
|
13
|
+
type PiCommandResult = { status: number | null; stdout: string; stderr: string; error?: Error };
|
|
14
|
+
|
|
15
|
+
/** Used by Pi auth and model facets so both execute the CLI without a shell. */
|
|
16
|
+
export function runPiCommand(args: string[]): PiCommandResult {
|
|
17
|
+
const result = spawn.sync('pi', args, {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
timeout: 10_000,
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
status: result.status,
|
|
24
|
+
stdout: String(result.stdout ?? ''),
|
|
25
|
+
stderr: String(result.stderr ?? ''),
|
|
26
|
+
error: result.error,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const normalizeHeader = (value: string): string => value.toLowerCase().replace(/[^a-z]/g, '');
|
|
31
|
+
|
|
32
|
+
/** Parses Pi's fixed-column `--list-models` table; exported for focused provider tests. */
|
|
33
|
+
export function parsePiModelsTable(output: string): ProviderModelOption[] {
|
|
34
|
+
const lines = output.split(/\r?\n/).filter((line) => line.trim());
|
|
35
|
+
const headerIndex = lines.findIndex((line) => /provider/i.test(line) && /model/i.test(line));
|
|
36
|
+
if (headerIndex < 0) return [];
|
|
37
|
+
|
|
38
|
+
const header = lines[headerIndex];
|
|
39
|
+
const matches = [...header.matchAll(/\S+/g)];
|
|
40
|
+
const columns = matches.map((match, index) => ({
|
|
41
|
+
name: normalizeHeader(match[0]),
|
|
42
|
+
start: match.index ?? 0,
|
|
43
|
+
end: matches[index + 1]?.index ?? Number.POSITIVE_INFINITY,
|
|
44
|
+
}));
|
|
45
|
+
const providerColumn = columns.find((column) => column.name === 'provider');
|
|
46
|
+
const modelColumn = columns.find((column) => column.name === 'model' || column.name === 'modelid');
|
|
47
|
+
const thinkingColumn = columns.find((column) => column.name === 'thinking');
|
|
48
|
+
const imageColumn = columns.find((column) => column.name === 'images' || column.name === 'image');
|
|
49
|
+
if (!providerColumn || !modelColumn) return [];
|
|
50
|
+
|
|
51
|
+
const readColumn = (line: string, column: typeof columns[number]): string => (
|
|
52
|
+
line.slice(column.start, Number.isFinite(column.end) ? column.end : undefined).trim()
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return lines.slice(headerIndex + 1).flatMap((line) => {
|
|
56
|
+
if (/^-+$/.test(line.replace(/\s/g, ''))) return [];
|
|
57
|
+
const provider = readColumn(line, providerColumn);
|
|
58
|
+
const model = readColumn(line, modelColumn);
|
|
59
|
+
if (!provider || !model) return [];
|
|
60
|
+
const value = `${provider}/${model}`;
|
|
61
|
+
const supportsThinking = thinkingColumn
|
|
62
|
+
? /^(yes|true|supported)$/i.test(readColumn(line, thinkingColumn))
|
|
63
|
+
: false;
|
|
64
|
+
const supportsImages = imageColumn
|
|
65
|
+
? /^(yes|true|supported)$/i.test(readColumn(line, imageColumn))
|
|
66
|
+
: false;
|
|
67
|
+
return [{
|
|
68
|
+
value,
|
|
69
|
+
label: model,
|
|
70
|
+
description: `${provider}${supportsImages ? ' · Images' : ''}`,
|
|
71
|
+
...(supportsThinking ? {
|
|
72
|
+
effort: {
|
|
73
|
+
default: 'medium',
|
|
74
|
+
values: THINKING_LEVELS.map((level) => ({ value: level })),
|
|
75
|
+
},
|
|
76
|
+
} : {}),
|
|
77
|
+
}];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readPiDefaultModel(): Promise<string | null> {
|
|
82
|
+
try {
|
|
83
|
+
const settings = readObjectRecord(JSON.parse(await readFile(
|
|
84
|
+
path.join(os.homedir(), '.pi', 'agent', 'settings.json'),
|
|
85
|
+
'utf8',
|
|
86
|
+
)));
|
|
87
|
+
const provider = readOptionalString(settings?.defaultProvider);
|
|
88
|
+
const model = readOptionalString(settings?.defaultModel);
|
|
89
|
+
return provider && model ? `${provider}/${model}` : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Dynamic Pi catalog adapter. Discovery is intentionally uncached because auth changes alter the list. */
|
|
96
|
+
export class PiProviderModels implements IProviderModels {
|
|
97
|
+
async getSupportedModels(): Promise<ProviderModelsDefinition> {
|
|
98
|
+
const result = runPiCommand(['--list-models']);
|
|
99
|
+
const options = result.status === 0 && !result.error ? parsePiModelsTable(result.stdout) : [];
|
|
100
|
+
const configuredDefault = await readPiDefaultModel();
|
|
101
|
+
const fallback = options[0]?.value ?? configuredDefault ?? '';
|
|
102
|
+
return {
|
|
103
|
+
OPTIONS: options,
|
|
104
|
+
DEFAULT: configuredDefault && options.some((option) => option.value === configuredDefault)
|
|
105
|
+
? configuredDefault
|
|
106
|
+
: fallback,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async getCurrentActiveModel(): Promise<ProviderCurrentActiveModel> {
|
|
111
|
+
const models = await this.getSupportedModels();
|
|
112
|
+
return { model: models.DEFAULT };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import spawn from 'cross-spawn';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
appendFilesInputTag,
|
|
8
|
+
isAllowedImageSourcePath,
|
|
9
|
+
normalizeImageDescriptors,
|
|
10
|
+
resolveImageAbsolutePath,
|
|
11
|
+
resolveImageMediaType,
|
|
12
|
+
} from '@/shared/image-attachments.js';
|
|
13
|
+
import type { AnyRecord, ProviderRuntimeContext, ProviderRuntimeWriter } from '@/shared/types.js';
|
|
14
|
+
import { createCompleteMessage, createNormalizedMessage, findTopmostGitRoot } from '@/shared/utils.js';
|
|
15
|
+
|
|
16
|
+
type PiChild = ReturnType<typeof spawn> & { aborted?: boolean };
|
|
17
|
+
|
|
18
|
+
const activePiProcesses = new Map<string, PiChild>();
|
|
19
|
+
const ABORT_GRACE_MS = 500;
|
|
20
|
+
|
|
21
|
+
const formatError = (value: unknown): string => value instanceof Error ? value.message : String(value);
|
|
22
|
+
|
|
23
|
+
async function buildPiImages(images: unknown, cwd: string): Promise<Array<{
|
|
24
|
+
type: 'image';
|
|
25
|
+
data: string;
|
|
26
|
+
mimeType: string;
|
|
27
|
+
}>> {
|
|
28
|
+
const result: Array<{ type: 'image'; data: string; mimeType: string }> = [];
|
|
29
|
+
for (const descriptor of normalizeImageDescriptors(images)) {
|
|
30
|
+
const mimeType = resolveImageMediaType(descriptor);
|
|
31
|
+
const resolvedPath = resolveImageAbsolutePath(cwd, descriptor.path);
|
|
32
|
+
if (!mimeType?.startsWith('image/') || !isAllowedImageSourcePath(resolvedPath, cwd)) continue;
|
|
33
|
+
try {
|
|
34
|
+
const canonicalPath = await fs.realpath(resolvedPath);
|
|
35
|
+
if (!isAllowedImageSourcePath(canonicalPath, cwd)) continue;
|
|
36
|
+
result.push({
|
|
37
|
+
type: 'image',
|
|
38
|
+
data: (await fs.readFile(canonicalPath)).toString('base64'),
|
|
39
|
+
mimeType,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn(`[Pi] Unable to attach image ${descriptor.path}: ${formatError(error)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function collectProjectSkillArgs(cwd: string): Promise<string[]> {
|
|
49
|
+
const skillDirs = [path.join(cwd, '.pi', 'skills')];
|
|
50
|
+
const repoRoot = await findTopmostGitRoot(cwd);
|
|
51
|
+
let current = path.resolve(cwd);
|
|
52
|
+
while (true) {
|
|
53
|
+
skillDirs.push(path.join(current, '.agents', 'skills'));
|
|
54
|
+
if (!repoRoot || current === path.resolve(repoRoot)) break;
|
|
55
|
+
const parent = path.dirname(current);
|
|
56
|
+
if (parent === current) break;
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
return skillDirs.filter(existsSync).flatMap((directory) => ['--skill', directory]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const readTokenBudget = (event: AnyRecord): AnyRecord | null => {
|
|
63
|
+
const messages = Array.isArray(event.messages) ? event.messages : [];
|
|
64
|
+
let input = 0;
|
|
65
|
+
let output = 0;
|
|
66
|
+
let cacheRead = 0;
|
|
67
|
+
let cacheWrite = 0;
|
|
68
|
+
for (const message of messages) {
|
|
69
|
+
if (!message || typeof message !== 'object') continue;
|
|
70
|
+
const usage = (message as AnyRecord).usage;
|
|
71
|
+
if (!usage || typeof usage !== 'object') continue;
|
|
72
|
+
const record = usage as AnyRecord;
|
|
73
|
+
input += Number(record.input ?? 0);
|
|
74
|
+
output += Number(record.output ?? 0);
|
|
75
|
+
cacheRead += Number(record.cacheRead ?? 0);
|
|
76
|
+
cacheWrite += Number(record.cacheWrite ?? 0);
|
|
77
|
+
}
|
|
78
|
+
const used = input + output + cacheRead + cacheWrite;
|
|
79
|
+
return used > 0 ? {
|
|
80
|
+
used,
|
|
81
|
+
inputTokens: input + cacheRead,
|
|
82
|
+
outputTokens: output,
|
|
83
|
+
breakdown: { input: input + cacheRead, output },
|
|
84
|
+
} : null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Starts one isolated Pi RPC process for a single chat.send operation. */
|
|
88
|
+
export async function runPi(
|
|
89
|
+
command: string,
|
|
90
|
+
options: AnyRecord,
|
|
91
|
+
writer: ProviderRuntimeWriter,
|
|
92
|
+
context: ProviderRuntimeContext,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const sessionId = typeof options.sessionId === 'string' && options.sessionId.trim()
|
|
95
|
+
? options.sessionId.trim()
|
|
96
|
+
: crypto.randomUUID();
|
|
97
|
+
const workingDir = typeof options.cwd === 'string' && options.cwd.trim()
|
|
98
|
+
? options.cwd
|
|
99
|
+
: typeof options.projectPath === 'string' && options.projectPath.trim()
|
|
100
|
+
? options.projectPath
|
|
101
|
+
: process.cwd();
|
|
102
|
+
const providerSessionId = context.resolveProviderSessionId(sessionId);
|
|
103
|
+
const resolvedModel = await context.resolveResumeModel(
|
|
104
|
+
sessionId,
|
|
105
|
+
typeof options.model === 'string' ? options.model : undefined,
|
|
106
|
+
);
|
|
107
|
+
const args = ['--mode', 'rpc', '--no-approve'];
|
|
108
|
+
if (providerSessionId) args.push('--session', providerSessionId);
|
|
109
|
+
else args.push('--session-id', sessionId);
|
|
110
|
+
if (resolvedModel) {
|
|
111
|
+
const slash = resolvedModel.indexOf('/');
|
|
112
|
+
if (slash > 0) args.push('--provider', resolvedModel.slice(0, slash));
|
|
113
|
+
args.push('--model', resolvedModel);
|
|
114
|
+
}
|
|
115
|
+
if (typeof options.effort === 'string' && options.effort !== 'default') {
|
|
116
|
+
args.push('--thinking', options.effort);
|
|
117
|
+
}
|
|
118
|
+
args.push(...await collectProjectSkillArgs(workingDir));
|
|
119
|
+
|
|
120
|
+
const images = await buildPiImages(options.images, workingDir);
|
|
121
|
+
const prompt = appendFilesInputTag(command?.trim() ?? '', options.files);
|
|
122
|
+
|
|
123
|
+
await new Promise<void>((resolve, reject) => {
|
|
124
|
+
let child: PiChild;
|
|
125
|
+
let stdoutBuffer = '';
|
|
126
|
+
let stderr = '';
|
|
127
|
+
let terminalSent = false;
|
|
128
|
+
let settled = false;
|
|
129
|
+
let agentEnded = false;
|
|
130
|
+
let protocolError: Error | null = null;
|
|
131
|
+
|
|
132
|
+
const cleanup = () => {
|
|
133
|
+
if (activePiProcesses.get(sessionId) === child) activePiProcesses.delete(sessionId);
|
|
134
|
+
};
|
|
135
|
+
const settle = (exitCode: number, error?: Error) => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
cleanup();
|
|
139
|
+
if (!terminalSent && !child.aborted) {
|
|
140
|
+
terminalSent = true;
|
|
141
|
+
writer.send(createCompleteMessage({ provider: 'pi', sessionId, exitCode }));
|
|
142
|
+
}
|
|
143
|
+
if (error) reject(error);
|
|
144
|
+
else resolve();
|
|
145
|
+
};
|
|
146
|
+
const emitError = (message: string) => writer.send(createNormalizedMessage({
|
|
147
|
+
kind: 'error', provider: 'pi', sessionId, content: message,
|
|
148
|
+
}));
|
|
149
|
+
const processLine = (line: string) => {
|
|
150
|
+
if (!line.trim()) return;
|
|
151
|
+
let event: AnyRecord;
|
|
152
|
+
try {
|
|
153
|
+
event = JSON.parse(line) as AnyRecord;
|
|
154
|
+
} catch {
|
|
155
|
+
protocolError = new Error('Pi RPC returned invalid JSONL output.');
|
|
156
|
+
emitError(protocolError.message);
|
|
157
|
+
child.stdin?.end();
|
|
158
|
+
setTimeout(() => { if (child.exitCode === null) child.kill(); }, ABORT_GRACE_MS).unref();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (event.type === 'response' && event.success === false) {
|
|
162
|
+
protocolError = new Error(
|
|
163
|
+
typeof event.error === 'string' ? event.error : 'Pi rejected the RPC request.',
|
|
164
|
+
);
|
|
165
|
+
emitError(protocolError.message);
|
|
166
|
+
child.stdin?.end();
|
|
167
|
+
setTimeout(() => { if (child.exitCode === null) child.kill(); }, ABORT_GRACE_MS).unref();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
for (const message of context.normalizeMessage(event, sessionId)) writer.send(message);
|
|
171
|
+
if (event.type === 'agent_end' && event.willRetry !== true) {
|
|
172
|
+
agentEnded = true;
|
|
173
|
+
const tokenBudget = readTokenBudget(event);
|
|
174
|
+
if (tokenBudget) writer.send(createNormalizedMessage({
|
|
175
|
+
kind: 'status', provider: 'pi', sessionId, text: 'token_budget', tokenBudget,
|
|
176
|
+
}));
|
|
177
|
+
if (!terminalSent) {
|
|
178
|
+
terminalSent = true;
|
|
179
|
+
writer.send(createCompleteMessage({ provider: 'pi', sessionId, exitCode: 0 }));
|
|
180
|
+
}
|
|
181
|
+
child.stdin?.end();
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
if (child.exitCode === null) child.kill();
|
|
184
|
+
}, ABORT_GRACE_MS).unref();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
child = spawn('pi', args, {
|
|
190
|
+
cwd: workingDir,
|
|
191
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
192
|
+
env: process.env,
|
|
193
|
+
windowsHide: true,
|
|
194
|
+
}) as PiChild;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const wrapped = new Error(`Unable to start Pi CLI: ${formatError(error)}`);
|
|
197
|
+
emitError(wrapped.message);
|
|
198
|
+
reject(wrapped);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
activePiProcesses.set(sessionId, child);
|
|
203
|
+
writer.setSessionId?.(sessionId);
|
|
204
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
205
|
+
stdoutBuffer += chunk.toString('utf8');
|
|
206
|
+
const lines = stdoutBuffer.split(/\r?\n/);
|
|
207
|
+
stdoutBuffer = lines.pop() ?? '';
|
|
208
|
+
for (const line of lines) processLine(line);
|
|
209
|
+
});
|
|
210
|
+
child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); });
|
|
211
|
+
child.on('error', (error) => {
|
|
212
|
+
const wrapped = new Error(`Pi CLI failed: ${error.message}`);
|
|
213
|
+
emitError(wrapped.message);
|
|
214
|
+
settle(1, wrapped);
|
|
215
|
+
});
|
|
216
|
+
child.on('close', (code) => {
|
|
217
|
+
if (stdoutBuffer.trim()) processLine(stdoutBuffer);
|
|
218
|
+
if (settled) {
|
|
219
|
+
cleanup();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (child.aborted) {
|
|
223
|
+
settle(0);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (agentEnded) {
|
|
227
|
+
settle(0);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (protocolError) {
|
|
231
|
+
settle(1, protocolError);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const exitCode = code ?? 1;
|
|
235
|
+
if (exitCode !== 0) {
|
|
236
|
+
const message = stderr.trim() || `Pi CLI exited with code ${exitCode}.`;
|
|
237
|
+
emitError(message);
|
|
238
|
+
settle(exitCode, new Error(message));
|
|
239
|
+
} else {
|
|
240
|
+
const error = new Error('Pi RPC exited before emitting agent_end.');
|
|
241
|
+
emitError(error.message);
|
|
242
|
+
settle(1, error);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
child.stdin?.write(`${JSON.stringify({
|
|
246
|
+
id: `prompt-${sessionId}`,
|
|
247
|
+
type: 'prompt',
|
|
248
|
+
message: prompt,
|
|
249
|
+
...(images.length > 0 ? { images } : {}),
|
|
250
|
+
})}\n`);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Sends Pi's RPC abort command, then force-terminates an unresponsive child. */
|
|
255
|
+
export function abortPiSession(sessionId: string): boolean {
|
|
256
|
+
const child = activePiProcesses.get(sessionId);
|
|
257
|
+
if (!child) return false;
|
|
258
|
+
child.aborted = true;
|
|
259
|
+
try {
|
|
260
|
+
child.stdin?.write(`${JSON.stringify({ id: `abort-${sessionId}`, type: 'abort' })}\n`);
|
|
261
|
+
} catch {
|
|
262
|
+
child.kill();
|
|
263
|
+
}
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
if (child.exitCode === null) child.kill();
|
|
266
|
+
}, ABORT_GRACE_MS).unref();
|
|
267
|
+
activePiProcesses.delete(sessionId);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const piRuntime = { run: runPi, abort: abortPiSession };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { sessionsDb } from '@/modules/database/index.js';
|
|
7
|
+
import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
|
|
8
|
+
import type { AnyRecord } from '@/shared/types.js';
|
|
9
|
+
import {
|
|
10
|
+
normalizeProviderTimestamp,
|
|
11
|
+
normalizeSessionName,
|
|
12
|
+
readObjectRecord,
|
|
13
|
+
readOptionalString,
|
|
14
|
+
} from '@/shared/utils.js';
|
|
15
|
+
|
|
16
|
+
const PI_SETTINGS_PATH = path.join(os.homedir(), '.pi', 'agent', 'settings.json');
|
|
17
|
+
const DEFAULT_PI_SESSION_DIR = path.join(os.homedir(), '.pi', 'agent', 'sessions');
|
|
18
|
+
|
|
19
|
+
const expandHome = (value: string): string => {
|
|
20
|
+
if (value === '~') return os.homedir();
|
|
21
|
+
if (value.startsWith(`~${path.sep}`) || value.startsWith('~/') || value.startsWith('~\\')) {
|
|
22
|
+
return path.join(os.homedir(), value.slice(2));
|
|
23
|
+
}
|
|
24
|
+
return path.isAbsolute(value) ? value : path.resolve(os.homedir(), value);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Resolves Pi's transcript root for the synchronizer and filesystem watcher. */
|
|
28
|
+
export function getPiSessionDirectory(): string {
|
|
29
|
+
const environmentDirectory = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
30
|
+
if (environmentDirectory) return expandHome(environmentDirectory);
|
|
31
|
+
try {
|
|
32
|
+
const settings = readObjectRecord(JSON.parse(fsSync.readFileSync(PI_SETTINGS_PATH, 'utf8')));
|
|
33
|
+
const configured = readOptionalString(settings?.sessionDir);
|
|
34
|
+
if (configured) return expandHome(configured);
|
|
35
|
+
} catch {
|
|
36
|
+
// Missing or partially-written settings use Pi's standard session root.
|
|
37
|
+
}
|
|
38
|
+
return DEFAULT_PI_SESSION_DIR;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const listJsonlFiles = async (root: string): Promise<string[]> => {
|
|
42
|
+
const files: string[] = [];
|
|
43
|
+
const visit = async (directory: string): Promise<void> => {
|
|
44
|
+
let entries;
|
|
45
|
+
try { entries = await readdir(directory, { withFileTypes: true }); } catch { return; }
|
|
46
|
+
await Promise.all(entries.map(async (entry) => {
|
|
47
|
+
const entryPath = path.join(directory, entry.name);
|
|
48
|
+
if (entry.isDirectory()) await visit(entryPath);
|
|
49
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) files.push(entryPath);
|
|
50
|
+
}));
|
|
51
|
+
};
|
|
52
|
+
await visit(root);
|
|
53
|
+
return files;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const firstUserText = (entries: AnyRecord[]): string | undefined => {
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.type !== 'message') continue;
|
|
59
|
+
const message = readObjectRecord(entry.message);
|
|
60
|
+
if (message?.role !== 'user') continue;
|
|
61
|
+
if (typeof message.content === 'string' && message.content.trim()) return message.content.trim();
|
|
62
|
+
if (Array.isArray(message.content)) {
|
|
63
|
+
const text = message.content.flatMap((part) => {
|
|
64
|
+
const block = readObjectRecord(part);
|
|
65
|
+
return block?.type === 'text' ? [readOptionalString(block.text) ?? ''] : [];
|
|
66
|
+
}).join('').trim();
|
|
67
|
+
if (text) return text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Indexes Pi's JSONL transcript tree into WindCli's stable sessions table. */
|
|
74
|
+
export class PiSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
75
|
+
async synchronize(since?: Date): Promise<number> {
|
|
76
|
+
const files = await listJsonlFiles(getPiSessionDirectory());
|
|
77
|
+
let processed = 0;
|
|
78
|
+
for (const filePath of files) {
|
|
79
|
+
if (since) {
|
|
80
|
+
try {
|
|
81
|
+
if ((await stat(filePath)).mtime < since) continue;
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (await this.synchronizeFile(filePath)) processed += 1;
|
|
87
|
+
}
|
|
88
|
+
return processed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async synchronizeFile(filePath: string): Promise<string | null> {
|
|
92
|
+
if (!filePath.endsWith('.jsonl')) return null;
|
|
93
|
+
try {
|
|
94
|
+
const entries = (await readFile(filePath, 'utf8')).split(/\r?\n/).flatMap((line) => {
|
|
95
|
+
if (!line.trim()) return [];
|
|
96
|
+
try { return [JSON.parse(line) as AnyRecord]; } catch { return []; }
|
|
97
|
+
});
|
|
98
|
+
const header = entries.find((entry) => entry.type === 'session');
|
|
99
|
+
const sessionId = readOptionalString(header?.id);
|
|
100
|
+
const projectPath = readOptionalString(header?.cwd);
|
|
101
|
+
if (!sessionId || !projectPath) return null;
|
|
102
|
+
|
|
103
|
+
const sessionInfo = [...entries].reverse().find((entry) => entry.type === 'session_info');
|
|
104
|
+
const title = readOptionalString(sessionInfo?.name)
|
|
105
|
+
?? readOptionalString(sessionInfo?.title)
|
|
106
|
+
?? firstUserText(entries);
|
|
107
|
+
const fileStats = await stat(filePath);
|
|
108
|
+
const createdAt = normalizeProviderTimestamp(header?.timestamp ?? fileStats.birthtime);
|
|
109
|
+
const updatedAt = normalizeProviderTimestamp(
|
|
110
|
+
[...entries].reverse().find((entry) => readOptionalString(entry.timestamp))?.timestamp ?? fileStats.mtime,
|
|
111
|
+
);
|
|
112
|
+
const pending = sessionsDb.getSessionByProviderSessionId(sessionId)
|
|
113
|
+
?? sessionsDb.getSessionById(sessionId)
|
|
114
|
+
?? sessionsDb.findLatestPendingAppSession('pi', projectPath);
|
|
115
|
+
if (pending && !pending.provider_session_id) {
|
|
116
|
+
sessionsDb.assignProviderSessionId(pending.session_id, sessionId);
|
|
117
|
+
}
|
|
118
|
+
const existing = sessionsDb.getSessionByProviderSessionId(sessionId) ?? sessionsDb.getSessionById(sessionId);
|
|
119
|
+
const fallback = 'Untitled Pi Session';
|
|
120
|
+
return sessionsDb.createSession(
|
|
121
|
+
sessionId,
|
|
122
|
+
'pi',
|
|
123
|
+
projectPath,
|
|
124
|
+
normalizeSessionName(existing?.custom_name || title, fallback),
|
|
125
|
+
createdAt,
|
|
126
|
+
updatedAt,
|
|
127
|
+
filePath,
|
|
128
|
+
);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn(`[PiProvider] Failed to synchronize ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|