msco-pi-lot 0.1.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/.env.example +7 -0
- package/.github/workflows/ci.yml +79 -0
- package/.github/workflows/publish.yml +45 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +44 -0
- package/src/core/config.ts +71 -0
- package/src/core/ids.ts +5 -0
- package/src/core/mask-secrets.ts +19 -0
- package/src/core/session-trace.ts +51 -0
- package/src/index.ts +65 -0
- package/src/protocol/messages.ts +150 -0
- package/src/provider.ts +96 -0
- package/src/runtime/prompt.ts +20 -0
- package/src/runtime/runtime-manager.ts +65 -0
- package/src/runtime/session-runtime.ts +730 -0
- package/src/runtime/session-store.ts +39 -0
- package/src/runtime/tool-protocol.ts +493 -0
- package/src/transport/conversation-service.ts +140 -0
- package/src/transport/websocket-client.ts +106 -0
- package/src/types.ts +61 -0
- package/test/config.test.ts +49 -0
- package/test/messages.test.ts +29 -0
- package/test/provider.test.ts +46 -0
- package/test/runtime.test.ts +597 -0
- package/test/session-store.test.ts +40 -0
- package/test/session-trace.test.ts +26 -0
- package/test/shims/pi-ai.ts +76 -0
- package/test/shims/pi-coding-agent.ts +1 -0
- package/test/tool-protocol.test.ts +212 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +15 -0
package/.env.example
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main", "master"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["main", "master"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint and Typecheck
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Use Node.js
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '18'
|
|
21
|
+
cache: 'npm'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Run linter
|
|
27
|
+
run: npm run lint --if-present
|
|
28
|
+
|
|
29
|
+
- name: Run type checks
|
|
30
|
+
run: npm run typecheck --if-present
|
|
31
|
+
|
|
32
|
+
build:
|
|
33
|
+
name: Build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
needs: lint
|
|
36
|
+
steps:
|
|
37
|
+
- name: Checkout repository
|
|
38
|
+
uses: actions/checkout@v4
|
|
39
|
+
|
|
40
|
+
- name: Use Node.js
|
|
41
|
+
uses: actions/setup-node@v4
|
|
42
|
+
with:
|
|
43
|
+
node-version: '18'
|
|
44
|
+
cache: 'npm'
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: npm ci
|
|
48
|
+
|
|
49
|
+
- name: Build project
|
|
50
|
+
env:
|
|
51
|
+
CI: true
|
|
52
|
+
run: npm run build --if-present
|
|
53
|
+
|
|
54
|
+
test:
|
|
55
|
+
name: Run tests matrix
|
|
56
|
+
needs: build
|
|
57
|
+
runs-on: ${{ matrix.os }}
|
|
58
|
+
strategy:
|
|
59
|
+
matrix:
|
|
60
|
+
node-version: [18, 20]
|
|
61
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout repository
|
|
64
|
+
uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- name: Use Node.js
|
|
67
|
+
uses: actions/setup-node@v4
|
|
68
|
+
with:
|
|
69
|
+
node-version: ${{ matrix.node-version }}
|
|
70
|
+
cache: 'npm'
|
|
71
|
+
|
|
72
|
+
- name: Install dependencies
|
|
73
|
+
run: npm ci
|
|
74
|
+
|
|
75
|
+
- name: Run tests
|
|
76
|
+
env:
|
|
77
|
+
CI: true
|
|
78
|
+
run: npm test --if-present
|
|
79
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
name: Publish to npm
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
if: "github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')"
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Use Node.js
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '20'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Build (TypeScript)
|
|
27
|
+
if: hashFiles('tsconfig.json') != ''
|
|
28
|
+
run: npm run build --if-present
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: npm test --if-present
|
|
32
|
+
|
|
33
|
+
- name: Configure npm auth
|
|
34
|
+
run: |
|
|
35
|
+
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
36
|
+
|
|
37
|
+
- name: Verify package.json not private
|
|
38
|
+
run: |
|
|
39
|
+
node -e "const p=require('./package.json'); if(p.private){console.error('package.json has private: true — aborting publish'); process.exit(1);} console.log('package.json OK');"
|
|
40
|
+
|
|
41
|
+
- name: Publish to npm
|
|
42
|
+
env:
|
|
43
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
44
|
+
run: |
|
|
45
|
+
npm publish --access public
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Gribben
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# msco-pi-lot
|
|
2
|
+
|
|
3
|
+
Microsoft Copilot provider extension for `pi`.
|
|
4
|
+
|
|
5
|
+
<img width="1094" height="1200" alt="image" src="https://github.com/user-attachments/assets/fb12fb42-bc43-4e18-b77d-4f1d27ababe1" />
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Install directly from GitHub:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pi install https://github.com/atomic-reactor/msco-pi-lot
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
You can also pin a ref:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pi install https://github.com/atomic-reactor/msco-pi-lot@main
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
After install, restart `pi` and select the `microsoft-copilot/copilot` model.
|
|
23
|
+
|
|
24
|
+
For interactive use, log in once from inside `pi`:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
/login microsoft-copilot
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Paste your Microsoft Copilot access token when prompted. `pi` stores the credential in `~/.pi/agent/auth.json`. You can remove it later with:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
/logout microsoft-copilot
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Interactive login only supports pasting an access token. For headless or non-interactive use, you can still set Copilot credentials in your shell or in a local `.env` file next to the installed package:
|
|
39
|
+
|
|
40
|
+
```dotenv
|
|
41
|
+
MICROSOFT_COPILOT_ACCESS_TOKEN=
|
|
42
|
+
MICROSOFT_COPILOT_COOKIE=
|
|
43
|
+
MICROSOFT_COPILOT_CONVERSATION_ID=
|
|
44
|
+
MICROSOFT_COPILOT_CLIENT_SESSION_ID=
|
|
45
|
+
MICROSOFT_COPILOT_MODE=reasoning
|
|
46
|
+
MICROSOFT_COPILOT_TRACE=0
|
|
47
|
+
MICROSOFT_COPILOT_TRACE_FILE=logs/copilot-session.ndjson
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Legacy `COPILOT_*` variable names are still accepted.
|
|
51
|
+
|
|
52
|
+
Only `MICROSOFT_COPILOT_ACCESS_TOKEN` is required. Cookie-based settings remain optional transport tweaks, not a login method.
|
|
53
|
+
|
|
54
|
+
## Behavior
|
|
55
|
+
|
|
56
|
+
- Registers one `pi` model: `microsoft-copilot/copilot`
|
|
57
|
+
- Maps `pi` thinking levels to Copilot modes:
|
|
58
|
+
- `off`, `minimal`, `low` -> `smart`
|
|
59
|
+
- `medium`, `high`, `xhigh` -> `reasoning`
|
|
60
|
+
- Bootstraps a Copilot conversation over HTTP when needed
|
|
61
|
+
- Persists conversation state per `pi` session
|
|
62
|
+
- Supports local tool use through a prompt-mediated tool loop
|
|
63
|
+
- Uses Copilot server config to size prompts conservatively against the live `maxTextMessageLength`
|
|
64
|
+
|
|
65
|
+
## Known Issues
|
|
66
|
+
|
|
67
|
+
- This is still a basic integration. It gets Microsoft Copilot working inside `pi`, but it is not yet on par with a full agentic coding agent.
|
|
68
|
+
- Microsoft Copilot will sometimes fail to respond at all. In those cases the request may stall or end without a useful answer, and retrying is often the only workaround.
|
|
69
|
+
- Microsoft Copilot will sometimes behave as if it is running in a browser context. When that happens it may try to inspect browser tabs or page state that do not exist in `pi`, which can cause the response to stall or go off track.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
Install dependencies:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run tests:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm test
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For local extension loading during development:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pi -e ./src/index.ts
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Tracing
|
|
92
|
+
|
|
93
|
+
Enable websocket and bootstrap tracing with:
|
|
94
|
+
|
|
95
|
+
```dotenv
|
|
96
|
+
MICROSOFT_COPILOT_TRACE=1
|
|
97
|
+
MICROSOFT_COPILOT_TRACE_FILE=logs/copilot-session.ndjson
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Trace output is masked, but you should still treat it as sensitive and keep it out of git.
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "msco-pi-lot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Microsoft Copilot provider extension for pi coding agent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"exports": "./src/index.ts",
|
|
9
|
+
"pi": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./src/index.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "vitest --run"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"pi",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"pi-package",
|
|
21
|
+
"microsoft-copilot",
|
|
22
|
+
"copilot-provider",
|
|
23
|
+
"coding-agent",
|
|
24
|
+
"msco",
|
|
25
|
+
"pi-plugin"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git@github.com:atomic-reactor/msco-pi-lot.git"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"dotenv": "^16.6.1",
|
|
33
|
+
"ws": "^8.18.3"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@mariozechner/pi-ai": "*",
|
|
37
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^24.5.2",
|
|
41
|
+
"typescript": "^5.9.2",
|
|
42
|
+
"vitest": "^3.2.4"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { maskCookie } from "./mask-secrets.js";
|
|
3
|
+
import type { CopilotConfig, CopilotMode } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = Object.freeze({
|
|
6
|
+
mode: "reasoning" as CopilotMode,
|
|
7
|
+
channel: "edge",
|
|
8
|
+
apiVersion: "2",
|
|
9
|
+
debug: false,
|
|
10
|
+
trace: false,
|
|
11
|
+
origin: "https://copilot.microsoft.com",
|
|
12
|
+
userAgent:
|
|
13
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const ALLOWED_MODES = new Set<CopilotMode>(["reasoning", "smart"]);
|
|
17
|
+
|
|
18
|
+
function parseBooleanFlag(value: string | undefined, fallback: boolean): boolean {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readEnv(env: NodeJS.ProcessEnv, canonical: string, legacy?: string): string | undefined {
|
|
27
|
+
return env[canonical] || (legacy ? env[legacy] : undefined);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeMode(value: string | undefined): CopilotMode {
|
|
31
|
+
const mode = (value || DEFAULTS.mode) as CopilotMode;
|
|
32
|
+
if (!ALLOWED_MODES.has(mode)) {
|
|
33
|
+
throw new Error(`Unsupported Copilot mode "${mode}". Expected one of: reasoning, smart`);
|
|
34
|
+
}
|
|
35
|
+
return mode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LoadConfigOptions {
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
loadDotEnv?: boolean;
|
|
41
|
+
dotenvPath?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadConfig(options: LoadConfigOptions = {}): CopilotConfig {
|
|
45
|
+
if (options.loadDotEnv !== false) {
|
|
46
|
+
dotenv.config({ path: options.dotenvPath, quiet: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const env = options.env ?? process.env;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
cookie: readEnv(env, "MICROSOFT_COPILOT_COOKIE", "COPILOT_COOKIE") || "",
|
|
53
|
+
conversationId: readEnv(env, "MICROSOFT_COPILOT_CONVERSATION_ID", "COPILOT_CONVERSATION_ID") || undefined,
|
|
54
|
+
clientSessionId: readEnv(env, "MICROSOFT_COPILOT_CLIENT_SESSION_ID", "COPILOT_CLIENT_SESSION_ID") || undefined,
|
|
55
|
+
mode: normalizeMode(readEnv(env, "MICROSOFT_COPILOT_MODE", "COPILOT_MODE")),
|
|
56
|
+
channel: readEnv(env, "MICROSOFT_COPILOT_CHANNEL", "COPILOT_CHANNEL") || DEFAULTS.channel,
|
|
57
|
+
apiVersion: readEnv(env, "MICROSOFT_COPILOT_API_VERSION", "COPILOT_API_VERSION") || DEFAULTS.apiVersion,
|
|
58
|
+
debug: parseBooleanFlag(readEnv(env, "MICROSOFT_COPILOT_DEBUG", "COPILOT_DEBUG"), DEFAULTS.debug),
|
|
59
|
+
trace: parseBooleanFlag(readEnv(env, "MICROSOFT_COPILOT_TRACE", "COPILOT_TRACE"), DEFAULTS.trace),
|
|
60
|
+
traceFile: readEnv(env, "MICROSOFT_COPILOT_TRACE_FILE", "COPILOT_TRACE_FILE") || undefined,
|
|
61
|
+
origin: DEFAULTS.origin,
|
|
62
|
+
userAgent: DEFAULTS.userAgent
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function maskConfigForLog(config: CopilotConfig): Record<string, unknown> {
|
|
67
|
+
return {
|
|
68
|
+
...config,
|
|
69
|
+
cookie: maskCookie(config.cookie)
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/core/ids.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function maskLongValue(value: string): string {
|
|
2
|
+
if (value.length <= 10) {
|
|
3
|
+
return "*".repeat(value.length);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function maskSecret(value: string): string {
|
|
10
|
+
return value ? maskLongValue(value) : "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function maskCookie(value: string): string {
|
|
14
|
+
if (!value) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value.replace(/=([^;]+)/g, "=***");
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdirSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { maskCookie, maskSecret } from "./mask-secrets.js";
|
|
4
|
+
|
|
5
|
+
function sanitize(value: unknown): unknown {
|
|
6
|
+
if (typeof value === "string") {
|
|
7
|
+
return value
|
|
8
|
+
.replace(/accessToken=([^&]+)/g, (_match, secret) => `accessToken=${maskSecret(secret)}`)
|
|
9
|
+
.replace(/("accessToken":")([^"]+)(")/g, `$1***$3`)
|
|
10
|
+
.replace(/("Authorization":"Bearer )([^"]+)(")/g, `$1***$3`)
|
|
11
|
+
.replace(/("Cookie":")([^"]+)(")/g, (_match, prefix, cookie, suffix) => `${prefix}${maskCookie(cookie)}${suffix}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.map(sanitize);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (value && typeof value === "object") {
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.entries(value as Record<string, unknown>).map(([key, entry]) => {
|
|
21
|
+
if (key === "accessToken" || key === "Authorization") {
|
|
22
|
+
return [key, "***"];
|
|
23
|
+
}
|
|
24
|
+
if (key === "cookie" || key === "Cookie") {
|
|
25
|
+
return [key, typeof entry === "string" ? maskCookie(entry) : "***"];
|
|
26
|
+
}
|
|
27
|
+
return [key, sanitize(entry)];
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SessionTraceWriter {
|
|
36
|
+
readonly filePath: string;
|
|
37
|
+
|
|
38
|
+
constructor(filePath?: string) {
|
|
39
|
+
this.filePath = filePath || join("logs", `copilot-session-${Date.now()}.ndjson`);
|
|
40
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
write(type: string, payload?: unknown): void {
|
|
44
|
+
const row = {
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
type,
|
|
47
|
+
payload: sanitize(payload)
|
|
48
|
+
};
|
|
49
|
+
appendFileSync(this.filePath, `${JSON.stringify(row)}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { loadConfig, maskConfigForLog } from "./core/config.js";
|
|
3
|
+
import { generateClientSessionId } from "./core/ids.js";
|
|
4
|
+
import { SessionTraceWriter } from "./core/session-trace.js";
|
|
5
|
+
import { createProviderConfig, PROVIDER_NAME } from "./provider.js";
|
|
6
|
+
import { CopilotRuntimeManager } from "./runtime/runtime-manager.js";
|
|
7
|
+
import { CopilotSessionStore, SESSION_ENTRY_TYPE } from "./runtime/session-store.js";
|
|
8
|
+
import type { PersistedCopilotState } from "./types.js";
|
|
9
|
+
|
|
10
|
+
function reconstructState(sessionStore: CopilotSessionStore, runtimeManager: CopilotRuntimeManager, ctx: ExtensionContext): void {
|
|
11
|
+
const state = sessionStore.reconstruct(ctx.sessionManager);
|
|
12
|
+
runtimeManager.updatePersistedState(state, ctx.sessionManager.getSessionId());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function seedFreshState(sessionStore: CopilotSessionStore, runtimeManager: CopilotRuntimeManager, ctx: ExtensionContext): void {
|
|
16
|
+
const state: PersistedCopilotState = {
|
|
17
|
+
version: 2,
|
|
18
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
19
|
+
conversationId: "",
|
|
20
|
+
clientSessionId: generateClientSessionId(),
|
|
21
|
+
updatedAt: new Date().toISOString()
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
sessionStore.set(state);
|
|
25
|
+
runtimeManager.updatePersistedState(state, state.sessionId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function microsoftCopilotExtension(pi: ExtensionAPI): void {
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
const traceWriter = config.trace ? new SessionTraceWriter(config.traceFile) : undefined;
|
|
31
|
+
const sessionStore = new CopilotSessionStore();
|
|
32
|
+
const runtimeManager = new CopilotRuntimeManager(
|
|
33
|
+
config,
|
|
34
|
+
(sessionId) => sessionStore.get(sessionId),
|
|
35
|
+
(state: PersistedCopilotState) => {
|
|
36
|
+
pi.appendEntry(SESSION_ENTRY_TYPE, state);
|
|
37
|
+
sessionStore.set(state);
|
|
38
|
+
},
|
|
39
|
+
{ traceWriter }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
traceWriter?.write("extension.loaded", { config: maskConfigForLog(config) });
|
|
43
|
+
|
|
44
|
+
pi.registerProvider(PROVIDER_NAME, createProviderConfig(runtimeManager));
|
|
45
|
+
|
|
46
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
47
|
+
reconstructState(sessionStore, runtimeManager, ctx);
|
|
48
|
+
});
|
|
49
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
50
|
+
if (_event.reason === "new") {
|
|
51
|
+
seedFreshState(sessionStore, runtimeManager, ctx);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
reconstructState(sessionStore, runtimeManager, ctx);
|
|
55
|
+
});
|
|
56
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
57
|
+
reconstructState(sessionStore, runtimeManager, ctx);
|
|
58
|
+
});
|
|
59
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
60
|
+
reconstructState(sessionStore, runtimeManager, ctx);
|
|
61
|
+
});
|
|
62
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
63
|
+
runtimeManager.disconnectSession(ctx.sessionManager.getSessionId());
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { CopilotMode, CopilotRequestConfig } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_FEATURES = ["partial-generated-images"];
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_CARDS = [
|
|
6
|
+
"weather",
|
|
7
|
+
"local",
|
|
8
|
+
"image",
|
|
9
|
+
"inlineImage",
|
|
10
|
+
"sports",
|
|
11
|
+
"video",
|
|
12
|
+
"inlineVideo",
|
|
13
|
+
"healthcareEntity",
|
|
14
|
+
"healthcareInfo",
|
|
15
|
+
"safetyHelpline",
|
|
16
|
+
"quiz",
|
|
17
|
+
"finance",
|
|
18
|
+
"recipe",
|
|
19
|
+
"personalArtifacts",
|
|
20
|
+
"flashcard",
|
|
21
|
+
"navigation",
|
|
22
|
+
"person",
|
|
23
|
+
"consentV2",
|
|
24
|
+
"composeEmail"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const SUPPORTED_UI_COMPONENTS = {
|
|
28
|
+
Badge: "1.2",
|
|
29
|
+
Basic: "1.2",
|
|
30
|
+
Box: "1.2",
|
|
31
|
+
Button: "1.2",
|
|
32
|
+
Card: "1.2",
|
|
33
|
+
Caption: "1.2",
|
|
34
|
+
Chart: "1.2",
|
|
35
|
+
Checkbox: "1.2",
|
|
36
|
+
Col: "1.2",
|
|
37
|
+
DatePicker: "1.2",
|
|
38
|
+
Divider: "1.2",
|
|
39
|
+
Form: "1.2",
|
|
40
|
+
Icon: "1.2",
|
|
41
|
+
Image: "1.2",
|
|
42
|
+
Label: "1.2",
|
|
43
|
+
ListView: "1.2",
|
|
44
|
+
ListViewItem: "1.2",
|
|
45
|
+
Map: "1.3",
|
|
46
|
+
Markdown: "1.2",
|
|
47
|
+
Pressable: "1.3",
|
|
48
|
+
RadioGroup: "1.2",
|
|
49
|
+
Row: "1.2",
|
|
50
|
+
Select: "1.2",
|
|
51
|
+
Spacer: "1.2",
|
|
52
|
+
Text: "1.2",
|
|
53
|
+
Textarea: "1.2",
|
|
54
|
+
Title: "1.2",
|
|
55
|
+
Transition: "1.2"
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const SUPPORTED_ACTIONS: string[] = [];
|
|
59
|
+
|
|
60
|
+
export function buildWebSocketUrl(config: CopilotRequestConfig, clientSessionId: string): URL {
|
|
61
|
+
const url = new URL("wss://copilot.microsoft.com/c/api/chat");
|
|
62
|
+
url.searchParams.set("api-version", config.apiVersion);
|
|
63
|
+
url.searchParams.set("clientSessionId", clientSessionId);
|
|
64
|
+
url.searchParams.set("accessToken", config.accessToken);
|
|
65
|
+
url.searchParams.set("channel", config.channel);
|
|
66
|
+
url.searchParams.set("edgetab", "1");
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildWebSocketHeaders(config: CopilotRequestConfig): Record<string, string> {
|
|
71
|
+
const headers: Record<string, string> = {
|
|
72
|
+
Origin: config.origin,
|
|
73
|
+
"User-Agent": config.userAgent
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (config.cookie) {
|
|
77
|
+
headers.Cookie = config.cookie;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildSetOptionsEvent(): Record<string, unknown> {
|
|
84
|
+
return {
|
|
85
|
+
event: "setOptions",
|
|
86
|
+
supportedFeatures: SUPPORTED_FEATURES,
|
|
87
|
+
supportedCards: SUPPORTED_CARDS,
|
|
88
|
+
supportedUIComponents: SUPPORTED_UI_COMPONENTS,
|
|
89
|
+
ads: null,
|
|
90
|
+
supportedActions: SUPPORTED_ACTIONS
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildReportLocalConsentsEvent(): Record<string, unknown> {
|
|
95
|
+
return {
|
|
96
|
+
event: "reportLocalConsents",
|
|
97
|
+
grantedConsents: []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildMessagePreviewEvent(input: {
|
|
102
|
+
conversationId: string;
|
|
103
|
+
prompt: string;
|
|
104
|
+
}): Record<string, unknown> {
|
|
105
|
+
return buildPromptEvent({
|
|
106
|
+
event: "messagePreview",
|
|
107
|
+
conversationId: input.conversationId,
|
|
108
|
+
prompt: input.prompt,
|
|
109
|
+
mode: "smart"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildSendEvent(input: {
|
|
114
|
+
conversationId: string;
|
|
115
|
+
prompt: string;
|
|
116
|
+
mode: CopilotMode;
|
|
117
|
+
}): Record<string, unknown> {
|
|
118
|
+
return buildPromptEvent({
|
|
119
|
+
event: "send",
|
|
120
|
+
conversationId: input.conversationId,
|
|
121
|
+
prompt: input.prompt,
|
|
122
|
+
mode: input.mode
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildPongEvent(input: { pingId?: string; lastEventId?: string }): Record<string, unknown> {
|
|
127
|
+
return {
|
|
128
|
+
event: "pong",
|
|
129
|
+
id: input.pingId || `${input.lastEventId || "0"}.0001`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildPromptEvent(input: {
|
|
134
|
+
event: "messagePreview" | "send";
|
|
135
|
+
conversationId: string;
|
|
136
|
+
prompt: string;
|
|
137
|
+
mode: CopilotMode;
|
|
138
|
+
}): Record<string, unknown> {
|
|
139
|
+
return {
|
|
140
|
+
event: input.event,
|
|
141
|
+
conversationId: input.conversationId,
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: input.prompt
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
mode: input.mode
|
|
149
|
+
};
|
|
150
|
+
}
|