janissary 0.5.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/LICENSE +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- package/package.json +138 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
Required Notice: Copyright Derrick Parkhurst (https://github.com/thirtysixthspan)
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Janissary
|
|
2
|
+
|
|
3
|
+
Janissary is an **Agentic Working Environment**. In other words, it is an application for supporting your work using workflows augmented with agents.
|
|
4
|
+
|
|
5
|
+
A [Janissary](https://en.wikipedia.org/wiki/Janissary) was an elite infantry soldier in the Ottoman Empire — a servant of the gate, loyal, and ever-ready.
|
|
6
|
+
|
|
7
|
+
See the [User Docs](https://thirtysixthspan.github.io/janissary/user-documentation/getting-started/application) for the full command reference, tab types, and automation/advanced-agent guides.
|
|
8
|
+
|
|
9
|
+
See the [Developer Docs](https://thirtysixthspan.github.io/janissary/developer-documentation/) for advanced usage, coding, linting and testing practices.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- macOS
|
|
14
|
+
- Node.js 24+
|
|
15
|
+
- Google Chrome
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npx janus
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install globally:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
npm install -g janissary
|
|
27
|
+
janus
|
|
28
|
+
```
|
package/agent-names.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[
|
|
2
|
+
"ahmed",
|
|
3
|
+
"akbar",
|
|
4
|
+
"aslan",
|
|
5
|
+
"basir",
|
|
6
|
+
"bekir",
|
|
7
|
+
"bilal",
|
|
8
|
+
"cafer",
|
|
9
|
+
"cahit",
|
|
10
|
+
"cavus",
|
|
11
|
+
"davud",
|
|
12
|
+
"demir",
|
|
13
|
+
"dogan",
|
|
14
|
+
"ekrem",
|
|
15
|
+
"emrah",
|
|
16
|
+
"ersin",
|
|
17
|
+
"farid",
|
|
18
|
+
"fariz",
|
|
19
|
+
"fikri",
|
|
20
|
+
"hakim",
|
|
21
|
+
"hamza",
|
|
22
|
+
"harun",
|
|
23
|
+
"idris",
|
|
24
|
+
"ilyas",
|
|
25
|
+
"imran",
|
|
26
|
+
"jabir",
|
|
27
|
+
"jalal",
|
|
28
|
+
"jamal",
|
|
29
|
+
"kadir",
|
|
30
|
+
"kamil",
|
|
31
|
+
"kasim",
|
|
32
|
+
"latif",
|
|
33
|
+
"lutfi",
|
|
34
|
+
"mahir",
|
|
35
|
+
"malik",
|
|
36
|
+
"murad",
|
|
37
|
+
"omair",
|
|
38
|
+
"orhan",
|
|
39
|
+
"osman",
|
|
40
|
+
"rasim",
|
|
41
|
+
"recep",
|
|
42
|
+
"rifat",
|
|
43
|
+
"sabri",
|
|
44
|
+
"salih",
|
|
45
|
+
"selim",
|
|
46
|
+
"tahir",
|
|
47
|
+
"timur",
|
|
48
|
+
"turan",
|
|
49
|
+
"yahya",
|
|
50
|
+
"yavuz",
|
|
51
|
+
"yusuf"
|
|
52
|
+
]
|
package/bin/janus.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
// Janissary runs as a local web app: this launcher boots the Node server (which opens the
|
|
7
|
+
// browser to a token-gated localhost URL) and stays attached so Ctrl+C shuts it down.
|
|
8
|
+
const root = path.join(import.meta.dirname, '..');
|
|
9
|
+
const compiled = path.join(root, 'dist', 'main.js');
|
|
10
|
+
const source = path.join(root, 'src', 'main.ts');
|
|
11
|
+
const localTsx = path.join(root, 'node_modules', 'tsx', 'dist', 'cli.mjs');
|
|
12
|
+
const arguments_ = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
let command, commandArguments;
|
|
15
|
+
if (existsSync(compiled)) {
|
|
16
|
+
command = process.execPath;
|
|
17
|
+
commandArguments = [compiled, ...arguments_];
|
|
18
|
+
} else if (existsSync(localTsx)) {
|
|
19
|
+
command = process.execPath;
|
|
20
|
+
commandArguments = [localTsx, source, ...arguments_];
|
|
21
|
+
} else {
|
|
22
|
+
command = 'npx';
|
|
23
|
+
commandArguments = ['tsx', source, ...arguments_];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = spawnSync(command, commandArguments, { stdio: 'inherit' });
|
|
27
|
+
process.exit(result.status ?? 1);
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Relays the text currently visible in this frame's viewport to the Janissary app's top window, so
|
|
2
|
+
// a monitor watching a page tab can see what the user is actually looking at right now. Runs in
|
|
3
|
+
// every frame (manifest `all_frames: true`), but only ever acts inside an embedded page tab's
|
|
4
|
+
// iframe — the app's own top-level window is never a nested frame of itself, so it is a no-op there.
|
|
5
|
+
(function () {
|
|
6
|
+
if (window.top === window.self) return;
|
|
7
|
+
|
|
8
|
+
var CAPTURE_DEBOUNCE_MS = 500;
|
|
9
|
+
var SOURCE = 'janissary-page-content';
|
|
10
|
+
var timer = null;
|
|
11
|
+
|
|
12
|
+
function isVisible(el) {
|
|
13
|
+
var style = window.getComputedStyle(el);
|
|
14
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
15
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
16
|
+
var rect = el.getBoundingClientRect();
|
|
17
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
18
|
+
return rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Walk the live DOM, collecting the direct text of elements that are both viewport-intersecting
|
|
22
|
+
// and not hidden by CSS, joined in document order. Off-screen and hidden content is skipped.
|
|
23
|
+
function collectVisibleText(root) {
|
|
24
|
+
var parts = [];
|
|
25
|
+
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
|
26
|
+
acceptNode: function (node) {
|
|
27
|
+
var tag = node.tagName;
|
|
28
|
+
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT') return NodeFilter.FILTER_REJECT;
|
|
29
|
+
return isVisible(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
var node = walker.nextNode();
|
|
33
|
+
while (node) {
|
|
34
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
35
|
+
var child = node.childNodes[i];
|
|
36
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
37
|
+
var text = child.textContent.trim();
|
|
38
|
+
if (text) parts.push(text);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
node = walker.nextNode();
|
|
42
|
+
}
|
|
43
|
+
return parts.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function capture() {
|
|
47
|
+
if (!document.body) return;
|
|
48
|
+
var text = document.title + '\n\n' + collectVisibleText(document.body);
|
|
49
|
+
window.top.postMessage({ source: SOURCE, url: window.location.href, text: text }, '*');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scheduleCapture() {
|
|
53
|
+
if (timer) clearTimeout(timer);
|
|
54
|
+
timer = setTimeout(capture, CAPTURE_DEBOUNCE_MS);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
scheduleCapture();
|
|
58
|
+
new MutationObserver(scheduleCapture).observe(document.documentElement, {
|
|
59
|
+
childList: true, subtree: true, characterData: true, attributes: true,
|
|
60
|
+
});
|
|
61
|
+
window.addEventListener('scroll', scheduleCapture, { passive: true, capture: true });
|
|
62
|
+
})();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Janissary Frame Enabler",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"description": "Removes X-Frame-Options and CSP frame-ancestors from embedded page responses so all sites render in page tabs.",
|
|
6
|
+
"permissions": ["declarativeNetRequest"],
|
|
7
|
+
"host_permissions": ["<all_urls>"],
|
|
8
|
+
"declarative_net_request": {
|
|
9
|
+
"rule_resources": [
|
|
10
|
+
{
|
|
11
|
+
"id": "framing",
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"path": "rules.json"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"content_scripts": [
|
|
18
|
+
{
|
|
19
|
+
"matches": ["<all_urls>"],
|
|
20
|
+
"all_frames": true,
|
|
21
|
+
"js": ["content-script.js"],
|
|
22
|
+
"run_at": "document_idle"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": 1,
|
|
4
|
+
"priority": 1,
|
|
5
|
+
"condition": {
|
|
6
|
+
"resourceTypes": ["sub_frame"]
|
|
7
|
+
},
|
|
8
|
+
"action": {
|
|
9
|
+
"type": "modifyHeaders",
|
|
10
|
+
"responseHeaders": [
|
|
11
|
+
{ "header": "x-frame-options", "operation": "remove" },
|
|
12
|
+
{ "header": "content-security-policy", "operation": "remove" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { Readable, Writable } from 'node:stream';
|
|
3
|
+
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
|
|
4
|
+
import { sandboxSpawn } from '../sandbox/index.js';
|
|
5
|
+
import { getGithubToken } from '../github-token.js';
|
|
6
|
+
import { decidePermission } from './tools.js';
|
|
7
|
+
/**
|
|
8
|
+
* Connect to an arbitrary ACP agent launched as a subprocess and drive it as an ACP
|
|
9
|
+
* client over stdio. This is agent-agnostic — the caller supplies the command to run
|
|
10
|
+
* (e.g. `npx @agentclientprotocol/claude-agent-acp` or `opencode acp`).
|
|
11
|
+
*
|
|
12
|
+
* Scope: streams `agent_message_chunk` text into the prompt handler and advertises no fs/terminal
|
|
13
|
+
* capabilities (so the agent never calls those back). Tool permission requests are denied unless
|
|
14
|
+
* `options.allowedTools` opts a classified web tool in (see acp-tools.ts) — undefined/empty (every
|
|
15
|
+
* non-monitor caller and every tool-less persona) denies every request, as before.
|
|
16
|
+
*
|
|
17
|
+
* `workspaceDir`/`offline` confine the subprocess to that workspace via a Seatbelt sandbox (see
|
|
18
|
+
* sandbox.ts); omitted (monitor sessions never set them), the command runs exactly as before.
|
|
19
|
+
*/
|
|
20
|
+
export function connectAcp(options) {
|
|
21
|
+
const baseEnv = options.env ? { ...process.env, ...options.env } : process.env;
|
|
22
|
+
const { command, args, env } = sandboxSpawn({
|
|
23
|
+
workspaceDir: options.workspaceDir,
|
|
24
|
+
offline: options.offline,
|
|
25
|
+
githubToken: options.workspaceDir ? getGithubToken() : undefined,
|
|
26
|
+
}, options.command, options.args, baseEnv);
|
|
27
|
+
const proc = spawn(command, args, {
|
|
28
|
+
cwd: options.cwd,
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
env,
|
|
31
|
+
});
|
|
32
|
+
proc.on('error', (error) => options.onError(`failed to start ACP agent: ${error.message}`));
|
|
33
|
+
// The current in-flight prompt's handlers; session updates are routed here.
|
|
34
|
+
let current;
|
|
35
|
+
const client = {
|
|
36
|
+
async sessionUpdate(parameters) {
|
|
37
|
+
const update = parameters.update;
|
|
38
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
39
|
+
current?.onChunk(update.content.text);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
// Deny every tool request unless the persona's allowlist opts a classified web tool in.
|
|
43
|
+
async requestPermission(params) {
|
|
44
|
+
return { outcome: decidePermission(options.allowedTools, params.toolCall, params.options) };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const input = Readable.toWeb(proc.stdout);
|
|
48
|
+
const output = Writable.toWeb(proc.stdin);
|
|
49
|
+
const conn = new ClientSideConnection(() => client, ndJsonStream(output, input));
|
|
50
|
+
let sessionId;
|
|
51
|
+
let ready;
|
|
52
|
+
const ensureSession = () => {
|
|
53
|
+
ready ??= (async () => {
|
|
54
|
+
const init = await conn.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
|
|
55
|
+
const response = await conn.newSession({ cwd: options.cwd, mcpServers: [] });
|
|
56
|
+
sessionId = response.sessionId;
|
|
57
|
+
// Provider from the agent's reported name (fallback: the command basename);
|
|
58
|
+
// model is best-effort from the session's current mode (ACP has no model field).
|
|
59
|
+
const provider = init.agentInfo?.name ?? options.command.replace(/^.*[\\/]/, '');
|
|
60
|
+
const modes = response.modes;
|
|
61
|
+
const model = modes
|
|
62
|
+
? modes.availableModes.find((m) => m.id === modes.currentModeId)?.name ?? modes.currentModeId
|
|
63
|
+
: undefined;
|
|
64
|
+
options.onConnect?.({ provider, model });
|
|
65
|
+
})();
|
|
66
|
+
return ready;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
prompt: (text, handlers) => {
|
|
70
|
+
current = handlers;
|
|
71
|
+
void (async () => {
|
|
72
|
+
try {
|
|
73
|
+
await ensureSession();
|
|
74
|
+
const response = await conn.prompt({ sessionId: sessionId, prompt: [{ type: 'text', text }] });
|
|
75
|
+
handlers.onEnd(response.stopReason);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
handlers.onError(error instanceof Error ? error.message : String(error));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (current === handlers)
|
|
82
|
+
current = undefined;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
},
|
|
86
|
+
kill: () => {
|
|
87
|
+
try {
|
|
88
|
+
proc.kill();
|
|
89
|
+
}
|
|
90
|
+
catch { /* already gone */ }
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/acp/loop.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Autonomous tool loop for an ACP agent: the agent emits a single command, the
|
|
2
|
+
// host runs it, feeds the output back, and repeats until the agent answers
|
|
3
|
+
// without a command (or a step cap is reached). The control flow is pure — all
|
|
4
|
+
// side effects (rendering the transcript, executing the command) are injected,
|
|
5
|
+
// which keeps it testable independently of the live agent.
|
|
6
|
+
/**
|
|
7
|
+
* Drive the loop. Each turn streams the agent reply, then looks for a command:
|
|
8
|
+
* if one is found and the step budget remains, it is executed, its output is
|
|
9
|
+
* fed back as a follow-up prompt, and the loop continues; otherwise the loop
|
|
10
|
+
* ends. The primer is prepended only to the very first prompt.
|
|
11
|
+
*/
|
|
12
|
+
export function runAcpToolLoop(session, userPrompt, dependencies, h) {
|
|
13
|
+
const maxSteps = dependencies.maxSteps ?? 8;
|
|
14
|
+
// Start a turn's transcript entry, then issue the prompt into it.
|
|
15
|
+
function turn(turnPrompt, isFirst, step) {
|
|
16
|
+
h.startTurn(isFirst);
|
|
17
|
+
promptOnce(turnPrompt, isFirst, step, 0);
|
|
18
|
+
}
|
|
19
|
+
// Send one prompt for the current turn. A freshly connected agent sometimes
|
|
20
|
+
// returns an empty first reply (cold start — the model/provider loads lazily on
|
|
21
|
+
// the first prompt); retry the first turn once, reusing the same entry, before
|
|
22
|
+
// treating an empty reply as a final (no-command) answer.
|
|
23
|
+
function promptOnce(turnPrompt, isFirst, step, attempt) {
|
|
24
|
+
let buffer = '';
|
|
25
|
+
const sent = isFirst && dependencies.primer ? `${dependencies.primer}\n\n${userPrompt}` : turnPrompt;
|
|
26
|
+
session.prompt(sent, {
|
|
27
|
+
onChunk: (text) => { buffer += text; h.chunk(buffer); },
|
|
28
|
+
onEnd: async () => {
|
|
29
|
+
if (!buffer.trim() && isFirst && attempt === 0) {
|
|
30
|
+
promptOnce(turnPrompt, isFirst, step, attempt + 1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const command = dependencies.extractCommand(buffer);
|
|
34
|
+
const display = filterCommandFromDisplay(buffer, command);
|
|
35
|
+
h.endTurn(display);
|
|
36
|
+
if (command && step < maxSteps) {
|
|
37
|
+
// Only await when the command is actually async (e.g. browser); a sync command
|
|
38
|
+
// (db) must complete in the same tick so callers/tests observing synchronously
|
|
39
|
+
// see the full loop.
|
|
40
|
+
const returnValue = dependencies.runCommand(command);
|
|
41
|
+
const result = returnValue instanceof Promise ? await returnValue : returnValue;
|
|
42
|
+
h.ranCommand(command, result);
|
|
43
|
+
const followUp = `Output of \`${command}\`:\n${result}\n\n` +
|
|
44
|
+
'If the task is complete, reply with the final answer and no command. ' +
|
|
45
|
+
'Otherwise issue the next command. ' +
|
|
46
|
+
'Be concise: do not explain what you are doing. Only output commands and the final answer.';
|
|
47
|
+
turn(followUp, false, step + 1);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
h.finished(command ? 'capped' : 'answered', maxSteps);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
onError: (message) => h.error(message),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
turn(userPrompt, true, 0);
|
|
57
|
+
}
|
|
58
|
+
function filterCommandFromDisplay(display, command) {
|
|
59
|
+
if (!command)
|
|
60
|
+
return display;
|
|
61
|
+
const lines = display.split('\n');
|
|
62
|
+
const cleaned = lines.map((l) => l.replace(/^[\s`$>]+/, '').replace(/`+\s*$/, '').trim());
|
|
63
|
+
const index = cleaned.indexOf(command);
|
|
64
|
+
if (index === -1)
|
|
65
|
+
return display;
|
|
66
|
+
lines.splice(index, 1);
|
|
67
|
+
// Remove adjacent code fence markers left behind by the removed command.
|
|
68
|
+
if (index < lines.length && /^`{3,}\s*$/.test(lines[index]))
|
|
69
|
+
lines.splice(index, 1);
|
|
70
|
+
if (index > 0 && /^`{3,}\s*$/.test(lines[index - 1]))
|
|
71
|
+
lines.splice(index - 1, 1);
|
|
72
|
+
while (lines.length > 0 && lines.at(-1).trim() === '')
|
|
73
|
+
lines.pop();
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { connectAcp } from './index.js';
|
|
2
|
+
import { runAcpToolLoop } from './loop.js';
|
|
3
|
+
import { extractBrowserCommand, BROWSER_PRIMER } from '../browser/command.js';
|
|
4
|
+
import { messageBus } from '../bus.js';
|
|
5
|
+
import { notify } from '../notifications.js';
|
|
6
|
+
import { isRateLimitError } from './rate-limit.js';
|
|
7
|
+
import { makeUpdateRunning } from './runner.js';
|
|
8
|
+
// The ACP agent the manager connects to and the model it runs. Hardcoded for now (the only provider
|
|
9
|
+
// wired up); the model string drives the `provider/model` label shown in the connections panel.
|
|
10
|
+
const ACP_COMMAND = 'opencode';
|
|
11
|
+
const ACP_ARGS = ['acp'];
|
|
12
|
+
const ACP_MODEL = 'google/gemini-3.1-flash-lite';
|
|
13
|
+
const RESPONSE_BEGIN = '━━━━━━━━━━ BEGIN MODEL RESPONSE ━━━━━━━━━━';
|
|
14
|
+
const RESPONSE_END = '━━━━━━━━━━ END MODEL RESPONSE ━━━━━━━━━━';
|
|
15
|
+
// Prefix a still-streaming buffer with the begin marker only — the end marker appears once the
|
|
16
|
+
// turn is actually finished, not on every intermediate chunk.
|
|
17
|
+
function delimitStreaming(text) {
|
|
18
|
+
return text ? `${RESPONSE_BEGIN}\n${text}` : text;
|
|
19
|
+
}
|
|
20
|
+
// Wrap a finished turn's text with both markers, so the model's own reply reads as a clearly
|
|
21
|
+
// bounded block distinct from tool-call output and other transcript content around it.
|
|
22
|
+
function delimitFinal(text) {
|
|
23
|
+
return text ? `${RESPONSE_BEGIN}\n${text}\n${RESPONSE_END}` : text;
|
|
24
|
+
}
|
|
25
|
+
// Split a `provider/model` config string into its parts; a bare `model` with no slash has no
|
|
26
|
+
// provider. Drives the connections-panel label.
|
|
27
|
+
function parseModel(model) {
|
|
28
|
+
const slash = model.indexOf('/');
|
|
29
|
+
return slash === -1 ? { model } : { provider: model.slice(0, slash), model: model.slice(slash + 1) };
|
|
30
|
+
}
|
|
31
|
+
// Owns the per-tab ACP sessions (keyed by tab label) and their reported model info. Sessions connect
|
|
32
|
+
// lazily on first use and persist across prompts; the manager spawns/reuses them, exposes the
|
|
33
|
+
// connection label, and tears them down. The prompt/tool-loop orchestration stays with the caller —
|
|
34
|
+
// the manager only hands back the live session.
|
|
35
|
+
export class AcpManager {
|
|
36
|
+
managers;
|
|
37
|
+
sessions = new Map();
|
|
38
|
+
info = new Map();
|
|
39
|
+
constructor(managers) {
|
|
40
|
+
this.managers = managers;
|
|
41
|
+
}
|
|
42
|
+
// Whether a tab has a connected (or connecting) ACP session. Drives the connections panel and completion.
|
|
43
|
+
has(label) {
|
|
44
|
+
return this.sessions.has(label);
|
|
45
|
+
}
|
|
46
|
+
// The `provider/model` (or bare `model`) string for a tab's session, or undefined when none is
|
|
47
|
+
// connected. Display-only; populated on the connection handshake.
|
|
48
|
+
label(label) {
|
|
49
|
+
const info = this.info.get(label);
|
|
50
|
+
if (!info)
|
|
51
|
+
return undefined;
|
|
52
|
+
return info.provider ? `${info.provider}/${info.model ?? ''}` : info.model;
|
|
53
|
+
}
|
|
54
|
+
// The tab's ACP session, connecting one on first use and reusing it thereafter. The agent runs in
|
|
55
|
+
// `cwd`; `hooks.onConnect` fires after the handshake, by which point the session's model info is
|
|
56
|
+
// recorded (so `label` resolves).
|
|
57
|
+
session(label, cwd, hooks) {
|
|
58
|
+
let session = this.sessions.get(label);
|
|
59
|
+
if (!session) {
|
|
60
|
+
const info = parseModel(ACP_MODEL);
|
|
61
|
+
const tab = this.managers.tab.tabs.find((t) => t.label === label);
|
|
62
|
+
session = connectAcp({
|
|
63
|
+
command: ACP_COMMAND, args: ACP_ARGS, cwd,
|
|
64
|
+
onError: hooks.onError,
|
|
65
|
+
onConnect: () => { this.info.set(label, info); hooks.onConnect(); },
|
|
66
|
+
env: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ model: ACP_MODEL }) },
|
|
67
|
+
workspaceDir: tab?.workspaceDir,
|
|
68
|
+
offline: tab?.offline,
|
|
69
|
+
});
|
|
70
|
+
this.sessions.set(label, session);
|
|
71
|
+
}
|
|
72
|
+
return session;
|
|
73
|
+
}
|
|
74
|
+
// Kill and forget a tab's session (and its info). Returns whether one was open — the
|
|
75
|
+
// `connection close acp` path re-renders and reports only when it actually closed one.
|
|
76
|
+
close(label) {
|
|
77
|
+
const session = this.sessions.get(label);
|
|
78
|
+
if (!session)
|
|
79
|
+
return false;
|
|
80
|
+
session.kill();
|
|
81
|
+
this.sessions.delete(label);
|
|
82
|
+
this.info.delete(label);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Kill every session and forget all info (app shutdown).
|
|
86
|
+
closeAll() {
|
|
87
|
+
for (const [, session] of this.sessions)
|
|
88
|
+
session.kill();
|
|
89
|
+
this.sessions.clear();
|
|
90
|
+
this.info.clear();
|
|
91
|
+
}
|
|
92
|
+
run(label, command, onDone) {
|
|
93
|
+
const prompt = command.replace(/^acp\b\s*/i, '').trim();
|
|
94
|
+
if (!prompt) {
|
|
95
|
+
this.managers.tab.append(label, { input: command, output: 'Usage: acp <prompt>.' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const session = this.session(label, this.managers.tab.cwdOf(label) ?? process.cwd(), {
|
|
99
|
+
onError: (m) => this.managers.tab.append(label, { input: '', output: `ACP: ${m}` }),
|
|
100
|
+
onConnect: () => messageBus.emit('state', { type: 'dirty' }),
|
|
101
|
+
});
|
|
102
|
+
const updateRunning = makeUpdateRunning(label, this.managers);
|
|
103
|
+
let lastAnswer = '';
|
|
104
|
+
runAcpToolLoop(session, prompt, {
|
|
105
|
+
primer: `${this.managers.database.primer}\n\n${BROWSER_PRIMER}\n\nWrite your replies in GitHub-flavored Markdown (headings, lists, tables, fenced code blocks, etc.); the tab renders them as formatted Markdown.`,
|
|
106
|
+
runCommand: (c) => (/^browser\b/i.test(c) ? this.managers.browser.run(label, c) : this.managers.database.runInTab(label, c)),
|
|
107
|
+
extractCommand: (t) => extractBrowserCommand(t) ?? this.managers.database.extract(t) ?? null,
|
|
108
|
+
}, {
|
|
109
|
+
startTurn: (isFirst) => { this.managers.tab.addBusy(label); if (isFirst)
|
|
110
|
+
notify(this.managers, 'agent-start', label); this.managers.tab.append(label, { input: isFirst ? prompt : '', output: '', running: true, markdown: true }); },
|
|
111
|
+
chunk: (buffer) => updateRunning(delimitStreaming(buffer), true),
|
|
112
|
+
endTurn: (final) => { updateRunning(delimitFinal(final), false); lastAnswer = final; },
|
|
113
|
+
ranCommand: (c, result) => this.managers.tab.append(label, { input: c, output: result, acp: true }),
|
|
114
|
+
finished: (reason, maxSteps) => {
|
|
115
|
+
this.managers.tab.deleteBusy(label);
|
|
116
|
+
notify(this.managers, 'state-change', label);
|
|
117
|
+
if (reason === 'capped')
|
|
118
|
+
this.managers.tab.append(label, { input: '', output: `(stopped after ${maxSteps} tool steps)` });
|
|
119
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
120
|
+
onDone?.(lastAnswer);
|
|
121
|
+
},
|
|
122
|
+
error: (m) => { updateRunning(`ACP error: ${m}`, false); this.managers.tab.deleteBusy(label); notify(this.managers, 'state-change', label); if (isRateLimitError(m))
|
|
123
|
+
notify(this.managers, 'rate-limited', label); onDone?.(`ACP error: ${m}`); },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Best-effort detection of a rate-limit failure from an ACP query's error message. The ACP
|
|
2
|
+
// protocol and its agent adapters carry no structured error code, so this is a case-insensitive
|
|
3
|
+
// match against a small set of markers known to appear in provider rate-limit messages.
|
|
4
|
+
const RATE_LIMIT_MARKERS = ['429', 'rate limit', 'too many requests'];
|
|
5
|
+
export function isRateLimitError(message) {
|
|
6
|
+
const lower = message.toLowerCase();
|
|
7
|
+
return RATE_LIMIT_MARKERS.some((marker) => lower.includes(marker));
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { messageBus } from '../bus.js';
|
|
2
|
+
export function makeUpdateRunning(label, managers) {
|
|
3
|
+
return (output, running) => {
|
|
4
|
+
const t = managers.tab.tabs.find((x) => x.label === label);
|
|
5
|
+
if (t) {
|
|
6
|
+
const log = [...t.log];
|
|
7
|
+
const index = log.findLastIndex((e) => e.running);
|
|
8
|
+
if (index !== -1)
|
|
9
|
+
log[index] = { ...log[index], output, running };
|
|
10
|
+
t.log = log;
|
|
11
|
+
if (!running)
|
|
12
|
+
managers.tab.persist(managers.tab.buildAgentState(t));
|
|
13
|
+
}
|
|
14
|
+
if (!running && output && t)
|
|
15
|
+
messageBus.emit('transcript', { type: 'entry:appended', tabLabel: label, entry: { input: '', output }, tab: t });
|
|
16
|
+
messageBus.emit('state', { type: 'dirty' });
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Classify a tool-permission request into a canonical web-tool id, or null when it is not
|
|
2
|
+
// confidently one of them (erring toward null keeps the deny boundary safe).
|
|
3
|
+
export function classifyTool(toolCall) {
|
|
4
|
+
const title = (toolCall.title ?? '').toLowerCase();
|
|
5
|
+
// web_fetch: `kind: 'fetch'` is unambiguous (retrieve a URL); also accept a title match for
|
|
6
|
+
// adapters that leave `kind` unset.
|
|
7
|
+
if (toolCall.kind === 'fetch' || /web[_ ]?fetch|webfetch/.test(title))
|
|
8
|
+
return 'web_fetch';
|
|
9
|
+
// web_search: `kind: 'search'` is ambiguous — a local file/codebase search also reports it — so
|
|
10
|
+
// require a title match; never map a bare `kind: 'search'` to web search.
|
|
11
|
+
if (/web[_ ]?search/.test(title))
|
|
12
|
+
return 'web_search';
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
// Decide the outcome for a monitor tool-permission request: approve only a classified web tool the
|
|
16
|
+
// persona's allowlist includes, choosing the least-privilege allow option (`allow_once`, falling
|
|
17
|
+
// back to `allow_always`); deny everything else. An undefined/empty allowlist — every non-monitor
|
|
18
|
+
// caller and every tool-less persona — denies unconditionally, exactly as before.
|
|
19
|
+
export function decidePermission(allowedTools, toolCall, options) {
|
|
20
|
+
if (!allowedTools || allowedTools.length === 0)
|
|
21
|
+
return { outcome: 'cancelled' };
|
|
22
|
+
const tool = classifyTool(toolCall);
|
|
23
|
+
if (!tool || !allowedTools.includes(tool))
|
|
24
|
+
return { outcome: 'cancelled' };
|
|
25
|
+
const option = options.find((o) => o.kind === 'allow_once') ?? options.find((o) => o.kind === 'allow_always');
|
|
26
|
+
return option ? { outcome: 'selected', optionId: option.optionId } : { outcome: 'cancelled' };
|
|
27
|
+
}
|